koishi-plugin-cchess 2.1.0 → 2.2.1

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # koishi-plugin-cchess
2
2
 
3
- 中国象棋,支持人人对战与皮卡鱼人机对战
3
+ 中国象棋 · 人人对战与皮卡鱼人机对战
4
4
 
5
5
  ## 安装
6
6
 
@@ -16,17 +16,18 @@ yarn add koishi-plugin-cchess
16
16
  | --- | --- |
17
17
  | `cchess.开始 [红/黑] [人人/人机]` | 入座并开局,双方就位后自动开战 |
18
18
  | `cchess.开始 <FEN>` | 摆谱,导入局面后再 `cchess.开始` 即可 |
19
+ | `cchess.落子 <着法>` | 按棋谱落子,如 `炮二平五` |
19
20
  | `cchess.悔棋 [同意/拒绝]` | 请求悔棋,也可直接回复同意或拒绝表决 |
20
21
  | `cchess.认输` | 认输 |
21
- | `cchess.结束` | 强制结束棋局 |
22
+ | `cchess.结束` | 结束正在进行的一局 |
22
23
  | `cchess.战绩 [@某人/榜] [胜场/输场] [人数]` | 查询战绩与排行榜 |
23
24
  | `cchess.查看云库残局 [DTM/DTC]` | 云库残局统计 |
24
25
 
25
26
  发送 `cchess.开始` 即入座,双方就位后自动开局,无需单独的加入与退出指令。
26
27
 
27
- 着法可直接发送,例如 `炮二平五` 或 `b2e2`。
28
+ 着法可直接发送,例如 `炮二平五` 或 `b2e2`,也可以写成 `cchess.落子 炮二平五`。
28
29
 
29
- 悔棋请求送出后,对方直接回复 `同意` 或 `拒绝` 即可表决,不必带指令前缀。
30
+ 悔棋请求送出后,对方直接回复 `同意` 或 `拒绝` 即可表决,不必带指令前缀;请求方发送 `cchess.悔棋 拒绝` 可撤下请求。
30
31
 
31
32
  ## 许可证
32
33
 
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.\u5F00\u59CB` \u5373\u53EF\u5165\u5EA7\uFF0C\u53CC\u65B9\u5C31\u4F4D\u540E\u81EA\u52A8\u5F00\u5C40\uFF1B`cchess.\u5F00\u59CB \u4EBA\u673A` \u76F4\u63A5\u6311\u6218\u76AE\u5361\u9C7C\u3002\u7740\u6CD5\u53EF\u76F4\u63A5\u53D1\u9001\uFF0C\u4F8B\u5982 `\u70AE\u4E8C\u5E73\u4E94` \u6216 `b2e2`\u3002\n\n\u6094\u68CB\u8BF7\u6C42\u9001\u51FA\u540E\uFF0C\u5BF9\u65B9\u65E0\u9700\u5E26\u6307\u4EE4\u524D\u7F00\uFF0C\u76F4\u63A5\u56DE\u590D `\u540C\u610F` \u6216 `\u62D2\u7EDD` \u5373\u53EF\u8868\u51B3\u3002\n\n## \u6307\u4EE4\n\n| \u6307\u4EE4 | \u8BF4\u660E |\n| --- | --- |\n| `cchess.\u5F00\u59CB [\u7EA2/\u9ED1] [\u4EBA\u4EBA/\u4EBA\u673A]` | \u5165\u5EA7\u5E76\u5F00\u5C40\uFF0C\u53CC\u65B9\u5C31\u4F4D\u540E\u81EA\u52A8\u5F00\u6218 |\n| `cchess.\u5F00\u59CB <FEN>` | \u6446\u8C31\uFF0C\u5BFC\u5165\u5C40\u9762\u540E\u518D `cchess.\u5F00\u59CB` \u5373\u53EF |\n| `cchess.\u6094\u68CB [\u540C\u610F/\u62D2\u7EDD]` | \u8BF7\u6C42\u6094\u68CB\uFF0C\u4E5F\u53EF\u76F4\u63A5\u56DE\u590D\u540C\u610F\u6216\u62D2\u7EDD\u8868\u51B3 |\n| `cchess.\u8BA4\u8F93` | \u8BA4\u8F93\u5E76\u7ED3\u675F\u5BF9\u5C40 |\n| `cchess.\u7ED3\u675F` | \u7ED3\u675F\u5F53\u524D\u5BF9\u5C40 |\n| `cchess.\u6218\u7EE9 [@\u67D0\u4EBA/\u699C] [\u80DC\u573A/\u8F93\u573A] [\u4EBA\u6570]` | \u67E5\u8BE2\u6218\u7EE9\u6216\u6392\u884C\u699C |\n| `cchess.\u67E5\u770B\u4E91\u5E93\u6B8B\u5C40 [DTM/DTC]` | \u67E5\u8BE2\u4E91\u5E93\u6B8B\u5C40\u7EDF\u8BA1 |";
3
+ export declare const usage = "## \u4F7F\u7528\n\n`cchess.\u5F00\u59CB` \u5373\u53EF\u5165\u5EA7\uFF0C\u53CC\u65B9\u5C31\u4F4D\u540E\u81EA\u52A8\u5F00\u5C40\uFF1B`cchess.\u5F00\u59CB \u4EBA\u673A` \u76F4\u63A5\u6311\u6218\u76AE\u5361\u9C7C\u3002\u7740\u6CD5\u53EF\u76F4\u63A5\u53D1\u9001\uFF0C\u4F8B\u5982 `\u70AE\u4E8C\u5E73\u4E94` \u6216 `b2e2`\uFF0C\u4E5F\u53EF\u4EE5\u5199\u6210 `cchess.\u843D\u5B50 \u70AE\u4E8C\u5E73\u4E94`\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\uFF1B\u8BF7\u6C42\u65B9\u53D1\u9001 `cchess.\u6094\u68CB \u62D2\u7EDD` \u53EF\u64A4\u4E0B\u8BF7\u6C42\u3002\n\n## \u6307\u4EE4\n\n| \u6307\u4EE4 | \u8BF4\u660E |\n| --- | --- |\n| `cchess.\u5F00\u59CB [\u7EA2/\u9ED1] [\u4EBA\u4EBA/\u4EBA\u673A]` | \u5165\u5EA7\u5E76\u5F00\u5C40\uFF0C\u53CC\u65B9\u5C31\u4F4D\u540E\u81EA\u52A8\u5F00\u6218 |\n| `cchess.\u5F00\u59CB <FEN>` | \u6446\u8C31\uFF0C\u5BFC\u5165\u5C40\u9762\u540E\u518D `cchess.\u5F00\u59CB` \u5373\u53EF |\n| `cchess.\u843D\u5B50 <\u7740\u6CD5>` | \u6309\u68CB\u8C31\u843D\u5B50\uFF0C\u5982 `\u70AE\u4E8C\u5E73\u4E94` |\n| `cchess.\u6094\u68CB [\u540C\u610F/\u62D2\u7EDD]` | \u8BF7\u6C42\u6094\u68CB\uFF0C\u4E5F\u53EF\u76F4\u63A5\u56DE\u590D\u540C\u610F\u6216\u62D2\u7EDD\u8868\u51B3 |\n| `cchess.\u8BA4\u8F93` | \u8BA4\u8F93\u5E76\u7ED3\u675F\u5BF9\u5C40 |\n| `cchess.\u7ED3\u675F` | \u7ED3\u675F\u6B63\u5728\u8FDB\u884C\u7684\u4E00\u5C40 |\n| `cchess.\u6218\u7EE9 [@\u67D0\u4EBA/\u699C] [\u80DC\u573A/\u8F93\u573A] [\u4EBA\u6570]` | \u67E5\u8BE2\u6218\u7EE9\u6216\u6392\u884C\u699C |\n| `cchess.\u67E5\u770B\u4E91\u5E93\u6B8B\u5C40 [DTM/DTC]` | \u67E5\u8BE2\u4E91\u5E93\u6B8B\u5C40\u7EDF\u8BA1 |";
4
4
  export declare const inject: string[];
5
5
  export interface Config {
6
6
  boardSkin: string;
@@ -11,6 +11,7 @@ export interface Config {
11
11
  defaultMaxLeaderboardEntries: number;
12
12
  retractDelay: number;
13
13
  isChessImageWithOutlineEnabled: boolean;
14
+ disableImages: boolean;
14
15
  imgScale: number;
15
16
  imageType: "png" | "jpeg" | "webp";
16
17
  }
package/lib/index.js CHANGED
@@ -234,6 +234,24 @@ var SHAPE = {
234
234
  full: 9999
235
235
  };
236
236
  var FONT_STACK = '"Roboto Flex", "Roboto", "Noto Sans SC", "PingFang SC", "Microsoft YaHei", "Source Han Sans SC", system-ui, sans-serif';
237
+ var TYPE = {
238
+ displayLarge: { size: 57, line: 64, weight: 400, tracking: -0.25 },
239
+ displayMedium: { size: 45, line: 52, weight: 400, tracking: 0 },
240
+ displaySmall: { size: 36, line: 44, weight: 400, tracking: 0 },
241
+ headlineLarge: { size: 32, line: 40, weight: 400, tracking: 0 },
242
+ headlineMedium: { size: 28, line: 36, weight: 400, tracking: 0 },
243
+ headlineSmall: { size: 24, line: 32, weight: 400, tracking: 0 },
244
+ titleLarge: { size: 22, line: 28, weight: 400, tracking: 0 },
245
+ titleMedium: { size: 16, line: 24, weight: 500, tracking: 0.15 },
246
+ titleSmall: { size: 14, line: 20, weight: 500, tracking: 0.1 },
247
+ bodyLarge: { size: 16, line: 24, weight: 400, tracking: 0.5 },
248
+ bodyMedium: { size: 14, line: 20, weight: 400, tracking: 0.25 },
249
+ bodySmall: { size: 12, line: 16, weight: 400, tracking: 0.4 },
250
+ labelLarge: { size: 14, line: 20, weight: 500, tracking: 0.1 },
251
+ labelMedium: { size: 12, line: 16, weight: 500, tracking: 0.5 },
252
+ labelSmall: { size: 11, line: 16, weight: 500, tracking: 0.5 }
253
+ };
254
+ var EMPHASIZED_WEIGHT = { display: 600, headline: 600, title: 600, label: 600 };
237
255
  var MEDAL = {
238
256
  gold: lch(58, 52, 85),
239
257
  silver: lch(66, 5, 260),
@@ -246,9 +264,9 @@ var fs = __toESM(require("fs"));
246
264
  var name = "cchess";
247
265
  var usage = `## 使用
248
266
 
249
- \`cchess.开始\` 即可入座,双方就位后自动开局;\`cchess.开始 人机\` 直接挑战皮卡鱼。着法可直接发送,例如 \`炮二平五\` 或 \`b2e2\`。
267
+ \`cchess.开始\` 即可入座,双方就位后自动开局;\`cchess.开始 人机\` 直接挑战皮卡鱼。着法可直接发送,例如 \`炮二平五\` 或 \`b2e2\`,也可以写成 \`cchess.落子 炮二平五\`。
250
268
 
251
- 悔棋请求送出后,对方无需带指令前缀,直接回复 \`同意\` 或 \`拒绝\` 即可表决。
269
+ 悔棋请求送出后,对方无需带指令前缀,直接回复 \`同意\` 或 \`拒绝\` 即可表决;请求方发送 \`cchess.悔棋 拒绝\` 可撤下请求。
252
270
 
253
271
  ## 指令
254
272
 
@@ -256,9 +274,10 @@ var usage = `## 使用
256
274
  | --- | --- |
257
275
  | \`cchess.开始 [红/黑] [人人/人机]\` | 入座并开局,双方就位后自动开战 |
258
276
  | \`cchess.开始 <FEN>\` | 摆谱,导入局面后再 \`cchess.开始\` 即可 |
277
+ | \`cchess.落子 <着法>\` | 按棋谱落子,如 \`炮二平五\` |
259
278
  | \`cchess.悔棋 [同意/拒绝]\` | 请求悔棋,也可直接回复同意或拒绝表决 |
260
279
  | \`cchess.认输\` | 认输并结束对局 |
261
- | \`cchess.结束\` | 结束当前对局 |
280
+ | \`cchess.结束\` | 结束正在进行的一局 |
262
281
  | \`cchess.战绩 [@某人/榜] [胜场/输场] [人数]\` | 查询战绩或排行榜 |
263
282
  | \`cchess.查看云库残局 [DTM/DTC]\` | 查询云库残局统计 |`;
264
283
  var inject = ["database", "canvas"];
@@ -270,11 +289,12 @@ var Config = import_koishi.Schema.object({
270
289
  allowFreePieceMovementInHumanMachineMode: import_koishi.Schema.boolean().default(false).description(`人机模式下,允许所有人自由移动棋子,开启后可以不入座直接开始人机对局。`),
271
290
  enableDirectInput: import_koishi.Schema.boolean().default(true).description(`对局中直接发送棋谱或「同意」「拒绝」即可落子与应答,无需指令前缀。`),
272
291
  defaultEngineThinkingDepth: import_koishi.Schema.number().min(0).max(100).default(10).description(`默认引擎思考深度,越高 AI 棋力越强,耗时也越长(小于 1 时按 1 计算)。由于 Nodejs 不支持 SIMD,所以不建议设置过高。`),
273
- defaultMaxLeaderboardEntries: import_koishi.Schema.number().min(0).default(10).description(`排行榜默认显示的人数。`),
292
+ defaultMaxLeaderboardEntries: import_koishi.Schema.number().min(0).default(4).description(`排行榜默认显示的人数。`),
274
293
  retractDelay: import_koishi.Schema.number().min(0).default(0).description(`自动撤回延迟(秒),0 表示不撤回。`),
275
294
  imgScale: import_koishi.Schema.number().min(1).default(1).description(`图片分辨率倍率。`),
276
295
  imageType: import_koishi.Schema.union(["png", "jpeg", "webp"]).default("png").description(`发送的图片格式。`),
277
- isChessImageWithOutlineEnabled: import_koishi.Schema.boolean().default(true).description(`给棋盘图片加上坐标外框。关闭后出图明显更快,但没有外框,玩起来可能会比较累。`)
296
+ isChessImageWithOutlineEnabled: import_koishi.Schema.boolean().default(true).description(`给棋盘图片加上坐标外框。关闭后出图明显更快,但没有外框,玩起来可能会比较累。`),
297
+ disableImages: import_koishi.Schema.boolean().default(false).description(`全部改用文本,不发送棋盘图片。`)
278
298
  });
279
299
  var SIDE_ICONS = { 红方: "🔴", 黑方: "⚫" };
280
300
  var MEDALS = ["🥇", "🥈", "🥉"];
@@ -291,7 +311,7 @@ function isLine(line) {
291
311
  }
292
312
  __name(isLine, "isLine");
293
313
  function panel(options) {
294
- const { icon = "♟️", title, at, body = [], tips = [], image } = options;
314
+ const { icon, title, at, body = [], tips = [], image } = options;
295
315
  const lines = [`${icon} ${title}`];
296
316
  if (at) lines.push(`@${at}`);
297
317
  lines.push(...body.filter(isLine));
@@ -306,6 +326,7 @@ function apply(ctx, config) {
306
326
  const engines = {};
307
327
  const enginePromises = {};
308
328
  const busyChannels = /* @__PURE__ */ new Set();
329
+ const regretTicket = /* @__PURE__ */ new Map();
309
330
  ctx.on("dispose", () => {
310
331
  const channelIds = Object.keys(engines);
311
332
  channelIds.forEach((channelId) => {
@@ -464,7 +485,19 @@ function apply(ctx, config) {
464
485
  ctx.command("cchess", "中国象棋 · 楚河汉界").action(async ({ session }) => {
465
486
  await session.execute(`cchess -h`);
466
487
  });
467
- ctx.command("cchess.开始 [args:text]", "入座并开局").usage("可选阵营「红」「黑」与对手「人人」「人机」;直接粘贴 FEN 串则按摆谱处理。").example("cchess.开始 红 人机").action(async ({ session }, args) => {
488
+ ctx.command("cchess.开始 [args:text]", "入座,双方就位后开局").usage("可选阵营「红」「黑」与对手「人人」「人机」;直接粘贴 FEN 串则按摆谱处理。").example("cchess.开始 红 人机").action(async ({ session }, args) => {
489
+ const { username, channelId } = session;
490
+ if (busyChannels.has(channelId)) {
491
+ return await sendMessage(session, busyPanel(username));
492
+ }
493
+ busyChannels.add(channelId);
494
+ try {
495
+ return await startGame(session, args);
496
+ } finally {
497
+ busyChannels.delete(channelId);
498
+ }
499
+ });
500
+ async function startGame(session, args) {
468
501
  const { username, userId, channelId } = session;
469
502
  await updateNameInPlayerRecord(userId, username);
470
503
  const gameRecord = await getGameRecord(channelId);
@@ -522,11 +555,10 @@ function apply(ctx, config) {
522
555
  title: "已入座,静候对手",
523
556
  at: username,
524
557
  body: [
525
- field("阵营", withSideIcon(selfSide)),
526
- field("当前人数", `${players.length} 人`),
558
+ field("阵营", withSideIcon(selfSide)) + " " + field("当前人数", `${players.length} 人`),
527
559
  !blackPlayers.length ? "黑方尚无人。" : "红方尚无人。"
528
560
  ],
529
- tips: ["好友发送「cchess.开始」补位,双方就位即自动开局", "「cchess.开始 黑」可指定阵营"]
561
+ tips: ["好友发送「cchess.开始」即可补位,指定阵营可发「cchess.开始 黑」"]
530
562
  }));
531
563
  }
532
564
  await ctx.database.set("cchess_game_records", { channelId }, { isStarted: true });
@@ -535,19 +567,16 @@ function apply(ctx, config) {
535
567
  icon: "✅",
536
568
  title: "楚河汉界,对局开始",
537
569
  body: [
538
- `${withSideIcon("红方")}(${redPlayers.length})`,
539
- ...redPlayers.map((player) => ` @${player.username}`),
540
- "",
541
- `${withSideIcon("黑方")}(${blackPlayers.length})`,
542
- ...blackPlayers.map((player) => ` @${player.username}`),
543
- "",
544
- field("先手", withSideIcon(sideString))
570
+ `${withSideIcon("红方")} · ${redPlayers.map((player) => `@${player.username}`).join("、")}`,
571
+ `${withSideIcon("黑方")} · ${blackPlayers.map((player) => `@${player.username}`).join("、")}`,
572
+ `先手 · ${withSideIcon(sideString)}`
545
573
  ],
546
574
  tips: ["直接发送着法即可落子,如「炮二平五」或「b2e2」"],
547
575
  image: await renderBoard(channelId)
548
576
  }));
549
- });
550
- ctx.command("cchess.结束", "结束当前对局").action(async ({ session }) => {
577
+ }
578
+ __name(startGame, "startGame");
579
+ ctx.command("cchess.结束", "结束正在进行的一局").userFields(["id", "name", "authority"]).action(async ({ session }) => {
551
580
  const { username, userId, channelId } = session;
552
581
  await updateNameInPlayerRecord(userId, username);
553
582
  const gameRecord = await getGameRecord(channelId);
@@ -560,7 +589,24 @@ function apply(ctx, config) {
560
589
  tips: ["发送「cchess.开始」入座,再开一局"]
561
590
  }));
562
591
  }
563
- await endGame(channelId);
592
+ const playerRecord = await ctx.database.get("cchess_gaming_player_records", { channelId, userId });
593
+ if (!playerRecord.length && (session.user?.authority ?? 0) < 2) {
594
+ return await sendMessage(session, panel({
595
+ icon: "⚠️",
596
+ title: "权限不够",
597
+ at: username,
598
+ body: ["只有本局棋手或权限 2 以上的人能结束这一局。"]
599
+ }));
600
+ }
601
+ if (busyChannels.has(channelId)) {
602
+ return await sendMessage(session, busyPanel(username));
603
+ }
604
+ busyChannels.add(channelId);
605
+ try {
606
+ await endGame(channelId);
607
+ } finally {
608
+ busyChannels.delete(channelId);
609
+ }
564
610
  return await sendMessage(session, panel({
565
611
  icon: "✅",
566
612
  title: "对局已结束",
@@ -569,6 +615,9 @@ function apply(ctx, config) {
569
615
  tips: ["发送「cchess.开始」重整旗鼓"]
570
616
  }));
571
617
  });
618
+ ctx.command("cchess.落子 [move:string]", "发送着法,如「炮二平五」").usage("着法可用中式纵线记谱,也可用坐标,如 `b2e2`。").example("cchess.落子 炮二平五").action(async ({ session }, move) => {
619
+ return await handleMove(session, move);
620
+ });
572
621
  async function handleMove(session, moveOperation) {
573
622
  const { username, userId, channelId } = session;
574
623
  await updateNameInPlayerRecord(userId, username);
@@ -577,12 +626,7 @@ function apply(ctx, config) {
577
626
  return await sendMessage(session, invalidMovePanel(username, "看不懂这一着"));
578
627
  }
579
628
  if (busyChannels.has(channelId)) {
580
- return await sendMessage(session, panel({
581
- icon: "⏳",
582
- title: "对局正在推演",
583
- at: username,
584
- body: ["上一着棋尚未结算,稍候片刻。"]
585
- }));
629
+ return await sendMessage(session, busyPanel(username));
586
630
  }
587
631
  busyChannels.add(channelId);
588
632
  try {
@@ -681,6 +725,7 @@ function apply(ctx, config) {
681
725
  }
682
726
  const newMoves = record.moveList.slice(movesBefore);
683
727
  return await sendMessage(session, panel({
728
+ icon: "✅",
684
729
  title: `第 ${record.moveList.length} 手`,
685
730
  body: [
686
731
  ...newMoves.map((move) => `${withSideIcon(convertTurnToString(move.side))} ${move.chnMoveName} ${move.move}`),
@@ -693,7 +738,7 @@ function apply(ctx, config) {
693
738
  }
694
739
  }
695
740
  __name(handleMove, "handleMove");
696
- ctx.command("cchess.悔棋 [decision:string]", "请求悔棋或答复对方").usage("请求送出后,对方直接回复「同意」或「拒绝」即可表决,无需指令前缀。").action(async ({ session }, decision) => {
741
+ ctx.command("cchess.悔棋 [decision:string]", "请求悔棋或答复对方").usage("请求送出后,对方直接回复「同意」或「拒绝」即可表决,无需指令前缀;请求方发送「cchess.悔棋 拒绝」可撤下请求。").action(async ({ session }, decision) => {
697
742
  const { username, userId, channelId } = session;
698
743
  await updateNameInPlayerRecord(userId, username);
699
744
  if (decision) {
@@ -757,7 +802,10 @@ function apply(ctx, config) {
757
802
  }));
758
803
  }
759
804
  await ctx.database.set("cchess_game_records", { channelId }, { isRegretRequest: true });
805
+ const ticket = (regretTicket.get(channelId) ?? 0) + 1;
806
+ regretTicket.set(channelId, ticket);
760
807
  ctx.setTimeout(async () => {
808
+ if (regretTicket.get(channelId) !== ticket) return;
761
809
  const current = await getGameRecord(channelId);
762
810
  if (!current?.isRegretRequest) return;
763
811
  await ctx.database.set("cchess_game_records", { channelId }, { isRegretRequest: false });
@@ -772,11 +820,14 @@ function apply(ctx, config) {
772
820
  icon: "⏳",
773
821
  title: "悔棋请求已送出",
774
822
  at: username,
775
- body: [field("等待答复", withSideIcon(sideString))],
776
- tips: ["对方回复「同意」或「拒绝」即可表决;30 秒等不到答复会自动作废"]
823
+ body: [
824
+ field("等待答复", withSideIcon(sideString)),
825
+ "对方回复「同意」或「拒绝」即可表决,30 秒等不到答复会自动作废。"
826
+ ],
827
+ tips: ["发送「cchess.悔棋 拒绝」可撤下这次请求"]
777
828
  }));
778
829
  });
779
- ctx.command("cchess.认输", "认输并结束对局").action(async ({ session }) => {
830
+ ctx.command("cchess.认输", "认输并判对方获胜").action(async ({ session }) => {
780
831
  const { username, userId, channelId } = session;
781
832
  await updateNameInPlayerRecord(userId, username);
782
833
  const gameRecord = await getGameRecord(channelId);
@@ -871,10 +922,8 @@ function apply(ctx, config) {
871
922
  at: session.username,
872
923
  body: [
873
924
  field("查询对象", username),
874
- field("胜 场", `${win} 局`),
875
- field("负 场", `${lose} 局`),
876
- field("总对局", `${total} 局`),
877
- field("胜 率", winRate)
925
+ field("胜 场", `${win} 局`) + " " + field("负 场", `${lose} 局`),
926
+ field("总对局", `${total} 局`) + " " + field("胜 率", winRate)
878
927
  ]
879
928
  }));
880
929
  });
@@ -884,7 +933,7 @@ function apply(ctx, config) {
884
933
  title: "对局尚未开始",
885
934
  at: username,
886
935
  body: ["当前频道还没有进行中的对局。"],
887
- tips: ["「cchess.开始」入座并对弈", "「cchess.开始 人机」挑战皮卡鱼"]
936
+ tips: ["「cchess.开始」入座对弈 · 「cchess.开始 人机」挑战皮卡鱼"]
888
937
  });
889
938
  }
890
939
  __name(notStartedPanel, "notStartedPanel");
@@ -894,7 +943,7 @@ function apply(ctx, config) {
894
943
  title: "对局已经开战",
895
944
  at: username,
896
945
  body: ["本局尚未结束,不必重开。"],
897
- tips: ["「cchess.认输」结束本局", "「cchess.结束」强制清盘"]
946
+ tips: ["「cchess.认输」结束本局 · 「cchess.结束」清盘"]
898
947
  });
899
948
  }
900
949
  __name(alreadyStartedPanel, "alreadyStartedPanel");
@@ -908,6 +957,15 @@ function apply(ctx, config) {
908
957
  });
909
958
  }
910
959
  __name(notJoinedPanel, "notJoinedPanel");
960
+ function busyPanel(username) {
961
+ return panel({
962
+ icon: "⏳",
963
+ title: "对局正在推演",
964
+ at: username,
965
+ body: ["上一着棋尚未结算,稍候片刻。"]
966
+ });
967
+ }
968
+ __name(busyPanel, "busyPanel");
911
969
  function analyzingPanel(username) {
912
970
  return panel({
913
971
  icon: "⏳",
@@ -933,7 +991,7 @@ function apply(ctx, config) {
933
991
  title: reason,
934
992
  at: username,
935
993
  body: ["请确认着法是否合乎棋规。"],
936
- tips: ["纵线:炮二平五 / 炮8平5", "坐标:b2e2", "详见 https://www.xqbase.com/protocol/cchess_move.htm"]
994
+ tips: ["纵线:炮二平五 / 炮8平5 · 坐标:b2e2", "详见 https://www.xqbase.com/protocol/cchess_move.htm"]
937
995
  });
938
996
  }
939
997
  __name(invalidMovePanel, "invalidMovePanel");
@@ -952,12 +1010,13 @@ function apply(ctx, config) {
952
1010
  icon: "⚠️",
953
1011
  title: "排行榜人数有误",
954
1012
  at: username,
955
- body: ["请输入不小于 0 的整数。"],
956
- tips: ["例如:cchess.战绩 榜 10"]
1013
+ body: ["请发送不小于 0 的整数。"],
1014
+ tips: ["发送「cchess.战绩 榜 10」可查询前 10 名"]
957
1015
  });
958
1016
  }
959
1017
  __name(invalidLeaderboardSizePanel, "invalidLeaderboardSizePanel");
960
1018
  async function renderBoard(channelId) {
1019
+ if (config.disableImages) return void 0;
961
1020
  try {
962
1021
  return import_koishi.h.image(await drawChessBoard(channelId), imageMimeType).toString();
963
1022
  } catch (error) {
@@ -981,8 +1040,7 @@ function apply(ctx, config) {
981
1040
  at,
982
1041
  body: [
983
1042
  ...extra,
984
- field("胜方", withSideIcon(convertTurnToString(winSide))),
985
- field("棋手", winners.length ? winners.map((player) => `@${player.username}`).join("、") : "皮卡鱼")
1043
+ field("胜方", withSideIcon(convertTurnToString(winSide))) + " " + field("棋手", winners.length ? winners.map((player) => `@${player.username}`).join("、") : "皮卡鱼")
986
1044
  ],
987
1045
  tips: ["发送「cchess.开始」再战一局"],
988
1046
  image
@@ -1023,8 +1081,7 @@ function apply(ctx, config) {
1023
1081
  body: [
1024
1082
  field("棋 手", withSideIcon(humanSide)),
1025
1083
  field("皮卡鱼", withSideIcon(engineSide)),
1026
- field("先 手", withSideIcon(sideString)),
1027
- field("思考深度", `${thinkingDepth} 层`)
1084
+ field("先 手", withSideIcon(sideString)) + " " + field("思考深度", `${thinkingDepth} 层`)
1028
1085
  ],
1029
1086
  tips: ["直接发送着法即可落子,如「炮二平五」或「b2e2」"],
1030
1087
  image: await renderBoard(channelId)
@@ -1049,6 +1106,15 @@ function apply(ctx, config) {
1049
1106
  if (playerRecord.length === 0) return await sendMessage(session, notJoinedPanel(username));
1050
1107
  const sideString = convertTurnToString(gameRecord.turn);
1051
1108
  if (playerRecord[0].side !== sideString) {
1109
+ if (!agree) {
1110
+ await ctx.database.set("cchess_game_records", { channelId }, { isRegretRequest: false });
1111
+ return await sendMessage(session, panel({
1112
+ icon: "✅",
1113
+ title: "悔棋请求已撤下",
1114
+ at: username,
1115
+ body: ["对局继续,落子无悔。"]
1116
+ }));
1117
+ }
1052
1118
  return await sendMessage(session, panel({
1053
1119
  icon: "⚠️",
1054
1120
  title: "无权表决",
@@ -1127,8 +1193,8 @@ function apply(ctx, config) {
1127
1193
  return await sendMessage(session, panel({
1128
1194
  icon,
1129
1195
  title,
1130
- body: topPlayers.length ? topPlayers.map((player, index) => `${MEDALS[index] ?? `${index + 1}.`} ${player.username} ${player[sortField]} 局`) : ["榜上尚无名姓,快去手谈一局。"],
1131
- tips: topPlayers.length ? [] : ["发送「cchess.开始」开启对局"]
1196
+ body: topPlayers.length ? topPlayers.map((player, index) => `${MEDALS[index] ?? `${index + 1}.`} ${player.username} ${player[sortField]} 局`) : ["榜上尚无名姓。", "第一局分出胜负,名字就会写在这里。"],
1197
+ tips: topPlayers.length ? [] : ["发送「cchess.开始」开启一局"]
1132
1198
  }));
1133
1199
  }
1134
1200
  __name(getLeaderboard, "getLeaderboard");
@@ -1650,23 +1716,25 @@ function apply(ctx, config) {
1650
1716
  const rowAt = /* @__PURE__ */ __name((row) => FRAME_BOARD.y + (row * CELL_WIDTH + BOARD_PADDING) * FRAME_ZOOM, "rowAt");
1651
1717
  context.textAlign = "center";
1652
1718
  context.textBaseline = "middle";
1653
- const label = /* @__PURE__ */ __name((text, x, y, size, color) => {
1654
- context.font = `600 ${size}px ${FONT_STACK}`;
1719
+ const labelFont = `${EMPHASIZED_WEIGHT.title} ${TYPE.titleLarge.size}px ${FONT_STACK}`;
1720
+ const labelSideFont = `${EMPHASIZED_WEIGHT.headline} ${TYPE.headlineSmall.size}px ${FONT_STACK}`;
1721
+ const label = /* @__PURE__ */ __name((text, x, y, font, color) => {
1722
+ context.font = font;
1655
1723
  context.fillStyle = color;
1656
1724
  context.fillText(text, x, y);
1657
1725
  }, "label");
1658
1726
  for (let col = 0; col < 9; col++) {
1659
1727
  const x = columnAt(isFlipBoard ? 8 - col : col);
1660
- label(FILES[col], x, 30, 22, FRAME.onSurfaceVariant);
1661
- label(String(col + 1), x, 64, 22, FRAME.onSurfaceVariant);
1662
- label(RED_FILES[col], x, FRAME_HEIGHT - 62, 24, FRAME.onSurfaceVariant);
1663
- label(FILES[col], x, FRAME_HEIGHT - 26, 22, FRAME.onSurfaceVariant);
1728
+ label(FILES[col], x, 30, labelFont, FRAME.onSurfaceVariant);
1729
+ label(String(col + 1), x, 64, labelFont, FRAME.onSurfaceVariant);
1730
+ label(RED_FILES[col], x, FRAME_HEIGHT - 62, labelSideFont, FRAME.onSurfaceVariant);
1731
+ label(FILES[col], x, FRAME_HEIGHT - 26, labelFont, FRAME.onSurfaceVariant);
1664
1732
  }
1665
1733
  for (let row = 0; row < 10; row++) {
1666
1734
  const y = rowAt(isFlipBoard ? 9 - row : row);
1667
1735
  const text = String(9 - row);
1668
- label(text, 19, y, 22, FRAME.onSurfaceVariant);
1669
- label(text, FRAME_WIDTH - 19, y, 22, FRAME.onSurfaceVariant);
1736
+ label(text, 19, y, labelFont, FRAME.onSurfaceVariant);
1737
+ label(text, FRAME_WIDTH - 19, y, labelFont, FRAME.onSurfaceVariant);
1670
1738
  }
1671
1739
  return canvas.toBuffer(imageMimeType);
1672
1740
  }
@@ -2150,7 +2218,7 @@ function apply(ctx, config) {
2150
2218
  }
2151
2219
  }
2152
2220
  __name(drawChessBoard, "drawChessBoard");
2153
- async function drawLineArrow(ctx2, fromX, fromY, toX, toY, color = "black", headSize = 20) {
2221
+ async function drawLineArrow(ctx2, fromX, fromY, toX, toY, color = FRAME.onSurface, headSize = 20) {
2154
2222
  var angle = Math.atan2(toY - fromY, toX - fromX);
2155
2223
  var headAngle = 45 * Math.PI / 180;
2156
2224
  ctx2.beginPath();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-cchess",
3
- "description": "Koishi 的中国象棋插件。",
4
- "version": "2.1.0",
3
+ "description": "Koishi 插件 · 中国象棋。",
4
+ "version": "2.2.1",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [