koishi-plugin-cchess 1.0.8 → 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\n`cchess.\u52A0\u5165` \u5165\u5EA7\uFF0C`cchess.\u5F00\u59CB.\u4EBA\u4EBA\u5BF9\u6218` \u6216 `cchess.\u5F00\u59CB.\u4EBA\u673A\u5BF9\u6218` \u5F00\u5C40\u3002\u7740\u6CD5\u53EF\u76F4\u63A5\u53D1\u9001\uFF0C\u5982\u70AE\u4E8C\u5E73\u4E94\u6216 `b2e2`\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\u5BF9\u6218` | \u4EBA\u4EBA\u5BF9\u6218 |\n| `cchess.\u5F00\u59CB.\u4EBA\u673A\u5BF9\u6218` | \u4EBA\u673A\u5BF9\u6218\uFF08\u76AE\u5361\u9C7C\uFF09 |\n| `cchess.\u79FB\u52A8 <\u7740\u6CD5>` | \u843D\u5B50 |\n| `cchess.\u6094\u68CB.\u8BF7\u6C42` | \u6094\u68CB |\n| `cchess.\u8BA4\u8F93` | \u8BA4\u8F93 |\n| `cchess.\u7ED3\u675F` | \u5F3A\u5236\u6E05\u76D8 |\n| `cchess.\u7F16\u8F91\u68CB\u76D8.\u5BFC\u5165 <FEN>` | \u5BFC\u5165\u5C40\u9762 |\n| `cchess.\u7F16\u8F91\u68CB\u76D8.\u5BFC\u51FA` | \u5BFC\u51FA\u5C40\u9762 |\n| `cchess.\u6392\u884C\u699C.\u603B\u80DC\u573A [\u4EBA\u6570]` | \u80DC\u573A\u699C |\n| `cchess.\u67E5\u8BE2\u73A9\u5BB6\u8BB0\u5F55 [@\u67D0\u4EBA]` | \u67E5\u770B\u6218\u7EE9 |";
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
@@ -28,22 +28,24 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
28
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
29
 
30
30
  // src/index.ts
31
- var index_exports = {};
32
- __export(index_exports, {
31
+ var src_exports = {};
32
+ __export(src_exports, {
33
33
  Config: () => Config,
34
34
  apply: () => apply,
35
35
  inject: () => inject,
36
36
  name: () => name,
37
37
  usage: () => usage
38
38
  });
39
- module.exports = __toCommonJS(index_exports);
39
+ module.exports = __toCommonJS(src_exports);
40
40
  var import_koishi = require("koishi");
41
41
  var path = __toESM(require("path"));
42
42
  var fs = __toESM(require("fs"));
43
43
  var name = "cchess";
44
44
  var usage = `## 使用
45
45
 
46
- \`cchess.加入\` 入座,\`cchess.开始.人人对战\` 或 \`cchess.开始.人机对战\` 开局。着法可直接发送,如炮二平五或 \`b2e2\`。
46
+ \`cchess.加入\` 入座,\`cchess.开始 人人\` 或 \`cchess.开始 人机\` 开局。着法可直接发送,例如 \`炮二平五\` 或 \`b2e2\`。
47
+
48
+ 悔棋请求送出后,对方无需带指令前缀,直接回复 \`同意\` 或 \`拒绝\` 即可表决。
47
49
 
48
50
  ## 指令
49
51
 
@@ -51,16 +53,17 @@ var usage = `## 使用
51
53
  | --- | --- |
52
54
  | \`cchess.加入 [红/黑]\` | 入座 |
53
55
  | \`cchess.退出\` | 开局前离席 |
54
- | \`cchess.开始.人人对战\` | 人人对战 |
55
- | \`cchess.开始.人机对战\` | 人机对战(皮卡鱼) |
56
+ | \`cchess.开始 [人人/人机]\` | 开局,默认人人对战 |
56
57
  | \`cchess.移动 <着法>\` | 落子 |
57
- | \`cchess.悔棋.请求\` | 悔棋 |
58
+ | \`cchess.悔棋\` | 请求悔棋 |
59
+ | \`cchess.悔棋 <同意/拒绝>\` | 表决悔棋,也可直接回复同意或拒绝 |
58
60
  | \`cchess.认输\` | 认输 |
59
- | \`cchess.结束\` | 强制清盘 |
60
- | \`cchess.编辑棋盘.导入 <FEN>\` | 导入局面 |
61
+ | \`cchess.结束\` | 强制结束棋局 |
62
+ | \`cchess.编辑棋盘 <FEN>\` | 导入局面,不带参数时查看 FEN 用法 |
61
63
  | \`cchess.编辑棋盘.导出\` | 导出局面 |
62
- | \`cchess.排行榜.总胜场 [人数]\` | 胜场榜 |
63
- | \`cchess.查询玩家记录 [@某人]\` | 查看战绩 |`;
64
+ | \`cchess.查看云库残局 [DTM/DTC]\` | 云库残局统计 |
65
+ | \`cchess.排行榜 [胜场/输场] [人数]\` | 排行榜 |
66
+ | \`cchess.查询玩家记录 [@某人]\` | 战绩 |`;
64
67
  var inject = ["database", "puppeteer", "canvas"];
65
68
  var pieceSkins = ["棋弈无限图形棋子", "云库木制棋子", "刘炳森红黑牛角隶书棋子", "木制棋子", "本纹隶书棋子", "银灰将军体棋子", "棋弈无限红绿正棋子", "棋弈无限红绿棋子", "棋天大圣棋子", "棋者象棋棋子", "楷书玉石棋子", "水墨青瓷棋子", "牛皮纸华康金文棋子", "皮卡鱼棋子", "红蓝祥隶棋子", "红黑精典无阴影棋子", "红黑精典棋子", "行书玉石棋子", "象甲棋子", "金属棋子篆体棋子", "镌刻华彩棋子", "鹏飞红黑棋子", "鹏飞经典棋子", "鹏飞绿龙棋子", "龙腾四海棋子"];
66
69
  var boardSkins = ["棋弈无限红绿棋盘", "一鸣惊人棋盘", "三星堆棋盘", "云库木制棋盘", "云心鹤眼棋盘", "兰亭集序无字棋盘", "兰亭集序棋盘", "凤舞九天棋盘", "卯兔添福棋盘", "叱咤风云棋盘", "君临天下棋盘", "壁画古梦棋盘", "大闹天宫棋盘", "天山共色棋盘", "女神之约棋盘", "小吕飞刀棋盘", "山海绘卷棋盘", "护眼绿棋盘", "星河璀璨棋盘", "木制棋盘", "本纹隶书白棋盘", "本纹隶书黑棋盘", "桃花源记棋盘", "棋天大圣棋盘", "棋天无大圣棋盘", "武侠江湖棋盘", "水墨青瓷棋盘", "清悠茶道棋盘", "游园惊梦棋盘", "牛转乾坤棋盘", "玉石太极棋盘", "皓月无痕棋盘", "皮卡鱼棋盘", "盲棋神迹棋盘", "空城计棋盘", "竹波烟雨棋盘", "经典木棋盘棋盘", "绿色棋盘", "象甲2023棋盘", "象甲征程棋盘", "象甲重燃棋盘", "金牛降世棋盘", "金虎贺岁棋盘", "金虎贺岁(王天一签名版)棋盘", "鎏金岁月棋盘", "霸王别姬棋盘", "鸿门宴棋盘", "鹏飞红黑棋盘", "鹏飞经典棋盘", "鹏飞绿龙棋盘", "龙腾天坛棋盘"];
@@ -198,6 +201,26 @@ function apply(ctx, config) {
198
201
  primary: "id",
199
202
  autoInc: true
200
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
+ });
201
224
  ctx.middleware(async (session, next) => {
202
225
  const { channelId, content, userId, username } = session;
203
226
  if (!channelId || !content) return next();
@@ -257,7 +280,7 @@ function apply(ctx, config) {
257
280
  title: "入座成功",
258
281
  at: username,
259
282
  body: [field("阵营", withSideIcon(choice)), field("当前人数", `${playersNum + 1} 人`)],
260
- tips: ["再次发送「cchess.加入 红 / 黑」即可换边", "人齐后发送「cchess.开始.人人对战」开局"]
283
+ tips: ["再次发送「cchess.加入 红 / 黑」即可换边", "人齐后发送「cchess.开始 人人」开局"]
261
284
  }));
262
285
  } else {
263
286
  await ctx.database.set("cchess_gaming_player_records", { channelId, userId }, { side: choice });
@@ -297,19 +320,19 @@ function apply(ctx, config) {
297
320
  }));
298
321
  }
299
322
  });
300
- ctx.command("cchess.开始", "开始游戏指令帮助").action(async ({ session }) => {
301
- await session.execute(`cchess.开始 -h`);
302
- });
303
- ctx.command("cchess.开始.人人对战", "开始人人对战").action(async ({ session }) => {
323
+ ctx.command("cchess.开始 [mode:string]", "开始对局,mode 可选「人人」或「人机」").action(async ({ session }, mode) => {
304
324
  const { username, userId, channelId } = session;
305
325
  await updateNameInPlayerRecord(userId, username);
306
326
  const gameRecord = await getGameRecord(channelId);
307
327
  if (gameRecord.isStarted) {
308
328
  return await sendMessage(session, alreadyStartedPanel(username));
309
329
  }
310
- void checkEngine(channelId);
311
330
  const playerRecords = await ctx.database.get("cchess_gaming_player_records", { channelId });
312
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);
313
336
  let redPlayers = playerRecords.filter((player) => player.side === "红方");
314
337
  let blackPlayers = playerRecords.filter((player) => player.side === "黑方");
315
338
  let assignNotice = "";
@@ -352,62 +375,6 @@ function apply(ctx, config) {
352
375
  image: await renderBoard(channelId)
353
376
  }));
354
377
  });
355
- ctx.command("cchess.开始.人机对战", "开始人机对战").action(async ({ session }) => {
356
- const { username, userId, channelId } = session;
357
- await updateNameInPlayerRecord(userId, username);
358
- const gameRecord = await getGameRecord(channelId);
359
- if (gameRecord.isStarted) {
360
- return await sendMessage(session, alreadyStartedPanel(username));
361
- }
362
- const playerRecords = await ctx.database.get("cchess_gaming_player_records", { channelId });
363
- const playersNum = playerRecords.length;
364
- const redPlayers = playerRecords.filter((player) => player.side === "红方");
365
- const blackPlayers = playerRecords.filter((player) => player.side === "黑方");
366
- if (playersNum < 1 && !config.allowFreePieceMovementInHumanMachineMode) {
367
- return await sendMessage(session, panel({
368
- icon: "⚠️",
369
- title: "尚无棋手入座",
370
- at: username,
371
- body: ["人机对战至少需要 1 位棋手。"],
372
- tips: ["发送「cchess.加入」即可入座挑战皮卡鱼"]
373
- }));
374
- }
375
- if (!await checkEngine(channelId)) {
376
- return await sendMessage(session, engineUnavailablePanel(username));
377
- }
378
- let humanSide;
379
- if (playersNum === 1) {
380
- humanSide = playerRecords[0].side === "红方" ? "红方" : "黑方";
381
- } else if (redPlayers.length !== blackPlayers.length) {
382
- humanSide = redPlayers.length > blackPlayers.length ? "红方" : "黑方";
383
- } else {
384
- humanSide = Math.random() < 0.5 ? "红方" : "黑方";
385
- }
386
- const engineSide = humanSide === "红方" ? "黑方" : "红方";
387
- for (const player of playerRecords) {
388
- if (player.side === humanSide) continue;
389
- await ctx.database.set("cchess_gaming_player_records", { channelId, userId: player.userId }, { side: humanSide });
390
- }
391
- await ctx.database.set("cchess_game_records", { channelId }, {
392
- isStarted: true,
393
- isEnginePlayRed: engineSide === "红方",
394
- isEnginePlayBlack: engineSide === "黑方"
395
- });
396
- const sideString = convertTurnToString(gameRecord.turn);
397
- if (engineSide === sideString) await requestEngineMove(channelId);
398
- return await sendMessage(session, panel({
399
- icon: "✅",
400
- title: "人机对局开始",
401
- body: [
402
- field("棋 手", withSideIcon(humanSide)),
403
- field("皮卡鱼", withSideIcon(engineSide)),
404
- field("先 手", withSideIcon(sideString)),
405
- field("思考深度", `${thinkingDepth} 层`)
406
- ],
407
- tips: ["直接发送着法即可落子,如「炮二平五」或「b2e2」"],
408
- image: await renderBoard(channelId)
409
- }));
410
- });
411
378
  ctx.command("cchess.结束", "强制结束游戏").action(async ({ session }) => {
412
379
  const { username, userId, channelId } = session;
413
380
  await updateNameInPlayerRecord(userId, username);
@@ -457,7 +424,7 @@ function apply(ctx, config) {
457
424
  title: "悔棋请求待决",
458
425
  at: username,
459
426
  body: ["对方的悔棋请求尚未答复,棋局暂歇。"],
460
- tips: ["发送「cchess.悔棋.同意」或「cchess.悔棋.拒绝」"]
427
+ tips: ["对方回复「同意」或「拒绝」即可表决"]
461
428
  }));
462
429
  }
463
430
  const turn = gameRecord.turn;
@@ -553,12 +520,20 @@ function apply(ctx, config) {
553
520
  busyChannels.delete(channelId);
554
521
  }
555
522
  });
556
- ctx.command("cchess.悔棋", "悔棋指令帮助").action(async ({ session }) => {
557
- await session.execute(`cchess.悔棋 -h`);
558
- });
559
- ctx.command("cchess.悔棋.请求", "请求悔棋").action(async ({ session }) => {
523
+ ctx.command("cchess.悔棋 [decision:string]", "请求悔棋;待决时由对方以「同意 / 拒绝」表决").action(async ({ session }, decision) => {
560
524
  const { username, userId, channelId } = session;
561
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
+ }
562
537
  const gameRecord = await getGameRecord(channelId);
563
538
  if (!gameRecord.isStarted) {
564
539
  return await sendMessage(session, notStartedPanel(username));
@@ -571,7 +546,8 @@ function apply(ctx, config) {
571
546
  icon: "⏳",
572
547
  title: "已有悔棋请求",
573
548
  at: username,
574
- body: ["正在等待对方答复,请勿重复请求。"]
549
+ body: ["正在等待对方答复,请勿重复请求。"],
550
+ tips: ["对方回复「同意」或「拒绝」即可表决"]
575
551
  }));
576
552
  }
577
553
  const playerRecord = await ctx.database.get("cchess_gaming_player_records", { channelId, userId });
@@ -613,35 +589,7 @@ function apply(ctx, config) {
613
589
  title: "悔棋请求已送出",
614
590
  at: username,
615
591
  body: [field("等待答复", withSideIcon(sideString))],
616
- tips: ["对方可发送「cchess.悔棋.同意」或「cchess.悔棋.拒绝」"]
617
- }));
618
- });
619
- ctx.command("cchess.悔棋.同意", "同意悔棋").action(async ({ session }) => {
620
- const { username, channelId } = session;
621
- const decision = await checkRegretDecision(session, "同意");
622
- if (typeof decision === "string") return await sendMessage(session, decision);
623
- await undoMove(channelId);
624
- await ctx.database.set("cchess_game_records", { channelId }, { isRegretRequest: false });
625
- return await sendMessage(session, panel({
626
- icon: "✅",
627
- title: "悔棋成功",
628
- at: username,
629
- body: ["已同意悔棋,棋子已归原位。"],
630
- tips: ["对局继续。"],
631
- image: await renderBoard(channelId)
632
- }));
633
- });
634
- ctx.command("cchess.悔棋.拒绝", "拒绝悔棋").action(async ({ session }) => {
635
- const { username, channelId } = session;
636
- const decision = await checkRegretDecision(session, "拒绝");
637
- if (typeof decision === "string") return await sendMessage(session, decision);
638
- await ctx.database.set("cchess_game_records", { channelId }, { isRegretRequest: false });
639
- return await sendMessage(session, panel({
640
- icon: "⚠️",
641
- title: "悔棋被拒",
642
- at: username,
643
- body: ["落子无悔,棋局继续。"],
644
- image: await renderBoard(channelId)
592
+ tips: ["对方回复「同意」或「拒绝」即可表决"]
645
593
  }));
646
594
  });
647
595
  ctx.command("cchess.认输", "认输").action(async ({ session }) => {
@@ -676,35 +624,28 @@ function apply(ctx, config) {
676
624
  });
677
625
  return await sendMessage(session, message);
678
626
  });
679
- ctx.command("cchess.查看云库残局", "云库残局指令帮助").action(async ({ session }) => {
680
- await session.execute(`cchess.查看云库残局 -h`);
681
- });
682
- ctx.command("cchess.查看云库残局.DTM统计", "云库残局 DTM 统计").action(async ({ session }) => {
683
- const { username, userId } = session;
684
- await updateNameInPlayerRecord(userId, username);
685
- return await sendMessage(session, panel({
686
- icon: "📋",
687
- title: "云库残局 · DTM 统计",
688
- at: username,
689
- body: ["DTM:距将死的步数统计。", "https://www.chessdb.cn/egtb_info_dtm.html"]
690
- }));
691
- });
692
- ctx.command("cchess.查看云库残局.DTC统计", "云库残局 DTC 统计").action(async ({ session }) => {
627
+ ctx.command("cchess.查看云库残局 [type:string]", "云库残局 DTM / DTC 统计").action(async ({ session }, type) => {
693
628
  const { username, userId } = session;
694
629
  await updateNameInPlayerRecord(userId, username);
630
+ const isDtc = /dtc/i.test(type ?? "");
695
631
  return await sendMessage(session, panel({
696
632
  icon: "📋",
697
- title: "云库残局 · DTC 统计",
633
+ title: `云库残局 · ${isDtc ? "DTC" : "DTM"} 统计`,
698
634
  at: username,
699
- 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"]
700
636
  }));
701
637
  });
702
- ctx.command("cchess.编辑棋盘", "编辑棋盘指令帮助").action(async ({ session }) => {
703
- await session.execute(`cchess.编辑棋盘 -h`);
704
- });
705
- ctx.command("cchess.编辑棋盘.导入 <fen:text>", "导入FEN串").action(async ({ session }, fen) => {
638
+ ctx.command("cchess.编辑棋盘 [fen:text]", "导入 FEN 局面,不带参数时查看 FEN 用法").action(async ({ session }, fen) => {
706
639
  const { username, userId, channelId } = session;
707
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
+ }
708
649
  const gameRecord = await getGameRecord(channelId);
709
650
  if (gameRecord.isStarted) {
710
651
  return await sendMessage(session, panel({
@@ -724,7 +665,7 @@ function apply(ctx, config) {
724
665
  body: ["请检查局面串的格式是否完整。"],
725
666
  tips: [
726
667
  "示例:rnbakabnr/9/1c5c1/p1p1p1p1p/9/9/P1P1P1P1P/1C5C1/9/RNBAKABNR w",
727
- "发送「cchess.编辑棋盘.使用方法」查看规范"
668
+ "发送「cchess.编辑棋盘」查看规范"
728
669
  ]
729
670
  }));
730
671
  }
@@ -748,7 +689,7 @@ function apply(ctx, config) {
748
689
  title: "棋盘摆放完毕",
749
690
  at: username,
750
691
  body: [field("轮走方", withSideIcon(convertTurnToString(record.turn)))],
751
- tips: ["发送「cchess.开始.人人对战」或「cchess.开始.人机对战」由此局面开战"],
692
+ tips: ["发送「cchess.开始 人人」或「cchess.开始 人机」由此局面开战"],
752
693
  image: await renderBoard(channelId)
753
694
  }));
754
695
  });
@@ -768,37 +709,27 @@ function apply(ctx, config) {
768
709
  title: "当前局面 FEN",
769
710
  at: username,
770
711
  body: [fenWithFullMove],
771
- tips: ["可用「cchess.编辑棋盘.导入」还原此局面"]
772
- }));
773
- });
774
- ctx.command("cchess.编辑棋盘.使用方法", "查看编辑棋盘的fen使用方法").action(async ({ session }) => {
775
- const { username, userId } = session;
776
- await updateNameInPlayerRecord(userId, username);
777
- return await sendMessage(session, panel({
778
- icon: "📋",
779
- title: "FEN 串使用方法",
780
- at: username,
781
- body: ["中国象棋 FEN 格式规范:", "https://www.xqbase.com/protocol/cchess_fen.htm"]
712
+ tips: ["可用「cchess.编辑棋盘 <FEN>」还原此局面"]
782
713
  }));
783
714
  });
784
- ctx.command("cchess.排行榜", "排行榜指令帮助").action(async ({ session }) => {
785
- await session.execute(`cchess.排行榜 -h`);
786
- });
787
- 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) => {
788
716
  const { userId, username } = session;
789
717
  await updateNameInPlayerRecord(userId, username);
790
- if (typeof number !== "number" || isNaN(number) || number < 0) {
791
- return await sendMessage(session, invalidLeaderboardSizePanel(username));
718
+ if (type && /^\d+$/.test(type)) {
719
+ number = Number(type);
720
+ type = "";
792
721
  }
793
- return await getLeaderboard(session, "win", "总胜场排行榜", "🏆", number);
794
- });
795
- ctx.command("cchess.排行榜.总输场 [number:number]", "查看玩家总输场排行榜").action(async ({ session }, number = config.defaultMaxLeaderboardEntries) => {
796
- const { userId, username } = session;
797
- await updateNameInPlayerRecord(userId, username);
798
722
  if (typeof number !== "number" || isNaN(number) || number < 0) {
799
723
  return await sendMessage(session, invalidLeaderboardSizePanel(username));
800
724
  }
801
- 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
+ );
802
733
  });
803
734
  ctx.command("cchess.查询玩家记录 [targetUser:text]", "查询玩家记录").action(async ({ session }, targetUser) => {
804
735
  let { userId, username } = session;
@@ -848,7 +779,7 @@ function apply(ctx, config) {
848
779
  title: "棋局尚未开始",
849
780
  at: username,
850
781
  body: ["当前频道还没有进行中的对局。"],
851
- tips: ["「cchess.加入」入座", "「cchess.开始.人人对战」与好友手谈", "「cchess.开始.人机对战」挑战皮卡鱼"]
782
+ tips: ["「cchess.加入」入座", "「cchess.开始 人人」与好友手谈", "「cchess.开始 人机」挑战皮卡鱼"]
852
783
  });
853
784
  }
854
785
  __name(notStartedPanel, "notStartedPanel");
@@ -907,7 +838,7 @@ function apply(ctx, config) {
907
838
  title: "皮卡鱼尚未就绪",
908
839
  at: username,
909
840
  body: ["引擎启动失败,暂时无法进行人机对战。"],
910
- tips: ["稍后重试,或改用「cchess.开始.人人对战」"]
841
+ tips: ["稍后重试,或改用「cchess.开始 人人」"]
911
842
  });
912
843
  }
913
844
  __name(engineUnavailablePanel, "engineUnavailablePanel");
@@ -917,7 +848,7 @@ function apply(ctx, config) {
917
848
  title: "榜单人数有误",
918
849
  at: username,
919
850
  body: ["请输入不小于 0 的整数。"],
920
- tips: ["例如:cchess.排行榜.总胜场 10"]
851
+ tips: ["例如:cchess.排行榜 胜场 10"]
921
852
  });
922
853
  }
923
854
  __name(invalidLeaderboardSizePanel, "invalidLeaderboardSizePanel");
@@ -948,34 +879,104 @@ function apply(ctx, config) {
948
879
  });
949
880
  }
950
881
  __name(settleVictory, "settleVictory");
951
- 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) {
952
934
  const { username, userId, channelId } = session;
953
935
  await updateNameInPlayerRecord(userId, username);
954
936
  const gameRecord = await getGameRecord(channelId);
955
- if (!gameRecord.isStarted) return notStartedPanel(username);
956
- 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));
957
939
  if (!gameRecord.isRegretRequest) {
958
- return panel({
940
+ return await sendMessage(session, panel({
959
941
  icon: "⚠️",
960
942
  title: "暂无悔棋请求",
961
943
  at: username,
962
- body: [`当前没有待答复的请求,无从${action}。`]
963
- });
944
+ body: [`当前没有待答复的请求,无从${agree ? "同意" : "拒绝"}。`]
945
+ }));
964
946
  }
965
947
  const playerRecord = await ctx.database.get("cchess_gaming_player_records", { channelId, userId });
966
- if (playerRecord.length === 0) return notJoinedPanel(username);
948
+ if (playerRecord.length === 0) return await sendMessage(session, notJoinedPanel(username));
967
949
  const sideString = convertTurnToString(gameRecord.turn);
968
950
  if (playerRecord[0].side !== sideString) {
969
- return panel({
951
+ return await sendMessage(session, panel({
970
952
  icon: "⚠️",
971
953
  title: "无权表决",
972
954
  at: username,
973
955
  body: ["悔棋请求正由对方等待答复。", field("应答方", withSideIcon(sideString))]
974
- });
956
+ }));
975
957
  }
976
- 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
+ }));
977
978
  }
978
- __name(checkRegretDecision, "checkRegretDecision");
979
+ __name(resolveRegret, "resolveRegret");
979
980
  async function replaceAtTags(session, content) {
980
981
  const atRegex = /<at id="(\d+)"(?: name="([^"]*)")?\/>/g;
981
982
  let match;
@@ -1173,9 +1174,9 @@ function apply(ctx, config) {
1173
1174
  isHistoryMode = true;
1174
1175
  }
1175
1176
  let moveId = targetMove.moveId;
1176
- let idx = originalState.moveList.findIndex(((m) => m.moveId == moveId));
1177
+ let idx = originalState.moveList.findIndex((m) => m.moveId == moveId);
1177
1178
  moveList = originalState.moveList.slice(0, idx + 1);
1178
- idx = originalState.movesAfterLastEat.findIndex(((m) => m.moveId == moveId));
1179
+ idx = originalState.movesAfterLastEat.findIndex((m) => m.moveId == moveId);
1179
1180
  if (idx != -1) {
1180
1181
  movesAfterLastEat = originalState.movesAfterLastEat.slice(0, idx + 1);
1181
1182
  } else {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-cchess",
3
- "description": "Koishi 的中国象棋插件。支持人人/人机对战。",
4
- "version": "1.0.8",
3
+ "description": "Koishi 的中国象棋插件。",
4
+ "version": "1.1.0",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [
package/readme.md DELETED
@@ -1,49 +0,0 @@
1
- koishi-plugin-cchess
2
- ====================
3
-
4
- [<img alt="github" src="https://img.shields.io/badge/github-araea/koishi__plugin__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
- `cchess.加入` 入座,`cchess.开始.人人对战` 或 `cchess.开始.人机对战` 开局。着法可直接发送,如炮二平五或 `b2e2`。
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.排行榜.总胜场 [人数]` | 胜场榜 |
28
- | `cchess.查询玩家记录 [@某人]` | 查看战绩 |
29
-
30
- ## QQ 群
31
-
32
- 956758505
33
-
34
- <br>
35
-
36
- #### License
37
-
38
- <sup>
39
- Licensed under either of <a href="LICENSE-APACHE">Apache License, Version
40
- 2.0</a> or <a href="LICENSE-MIT">MIT license</a> at your option.
41
- </sup>
42
-
43
- <br>
44
-
45
- <sub>
46
- Unless you explicitly state otherwise, any contribution intentionally submitted
47
- for inclusion in this crate by you, as defined in the Apache-2.0 license, shall
48
- be dual licensed as above, without any additional terms or conditions.
49
- </sub>