koishi-plugin-cchess 2.0.1 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -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
+ | `cchess.结束` | 结束正在进行的一局 |
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,15 +1,17 @@
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;
7
7
  pieceSkin: string;
8
8
  defaultEngineThinkingDepth: number;
9
9
  allowFreePieceMovementInHumanMachineMode: boolean;
10
+ enableDirectInput: boolean;
10
11
  defaultMaxLeaderboardEntries: number;
11
12
  retractDelay: number;
12
13
  isChessImageWithOutlineEnabled: boolean;
14
+ disableImages: boolean;
13
15
  imgScale: number;
14
16
  imageType: "png" | "jpeg" | "webp";
15
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"];
@@ -268,14 +287,15 @@ var Config = import_koishi.Schema.object({
268
287
  boardSkin: import_koishi.Schema.union(boardSkins).default("象甲2023棋盘").description(`棋盘皮肤。`),
269
288
  pieceSkin: import_koishi.Schema.union(pieceSkins).default("象甲棋子").description(`棋子皮肤。`),
270
289
  allowFreePieceMovementInHumanMachineMode: import_koishi.Schema.boolean().default(false).description(`人机模式下,允许所有人自由移动棋子,开启后可以不入座直接开始人机对局。`),
290
+ enableDirectInput: import_koishi.Schema.boolean().default(true).description(`对局中直接发送棋谱或「同意」「拒绝」即可落子与应答,无需指令前缀。`),
271
291
  defaultEngineThinkingDepth: import_koishi.Schema.number().min(0).max(100).default(10).description(`默认引擎思考深度,越高 AI 棋力越强,耗时也越长(小于 1 时按 1 计算)。由于 Nodejs 不支持 SIMD,所以不建议设置过高。`),
272
- defaultMaxLeaderboardEntries: import_koishi.Schema.number().min(0).default(10).description(`排行榜默认显示的人数。`),
292
+ defaultMaxLeaderboardEntries: import_koishi.Schema.number().min(0).default(4).description(`排行榜默认显示的人数。`),
273
293
  retractDelay: import_koishi.Schema.number().min(0).default(0).description(`自动撤回延迟(秒),0 表示不撤回。`),
274
294
  imgScale: import_koishi.Schema.number().min(1).default(1).description(`图片分辨率倍率。`),
275
295
  imageType: import_koishi.Schema.union(["png", "jpeg", "webp"]).default("png").description(`发送的图片格式。`),
276
- 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(`全部改用文本,不发送棋盘图片。`)
277
298
  });
278
- var RULE = "━━━━━━━━━━━━━━";
279
299
  var SIDE_ICONS = { 红方: "🔴", 黑方: "⚫" };
280
300
  var MEDALS = ["🥇", "🥈", "🥉"];
281
301
  function withSideIcon(side) {
@@ -291,12 +311,12 @@ function isLine(line) {
291
311
  }
292
312
  __name(isLine, "isLine");
293
313
  function panel(options) {
294
- const { icon = "♟️", title, at, body = [], tips = [], image } = options;
295
- const lines = [`${icon} ${title}`, RULE];
314
+ const { icon, title, at, body = [], tips = [], image } = options;
315
+ const lines = [`${icon} ${title}`];
296
316
  if (at) lines.push(`@${at}`);
297
317
  lines.push(...body.filter(isLine));
298
318
  const validTips = tips.filter(isLine).filter(Boolean);
299
- if (validTips.length) lines.push(RULE, ...validTips);
319
+ if (validTips.length) lines.push(...validTips);
300
320
  if (image) lines.push(image);
301
321
  return lines.join("\n");
302
322
  }
@@ -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) => {
@@ -430,6 +451,7 @@ function apply(ctx, config) {
430
451
  }
431
452
  __name(matchRegretDecision, "matchRegretDecision");
432
453
  ctx.middleware(async (session, next) => {
454
+ if (!config.enableDirectInput) return next();
433
455
  const { channelId, content, userId } = session;
434
456
  if (!channelId || !content) return next();
435
457
  const decision = matchRegretDecision(content);
@@ -441,6 +463,7 @@ function apply(ctx, config) {
441
463
  await resolveRegret(session, decision === "agree");
442
464
  });
443
465
  ctx.middleware(async (session, next) => {
466
+ if (!config.enableDirectInput) return next();
444
467
  const { channelId, content, userId, username } = session;
445
468
  if (!channelId || !content) return next();
446
469
  const moveOperation = content.trim();
@@ -462,7 +485,19 @@ function apply(ctx, config) {
462
485
  ctx.command("cchess", "中国象棋 · 楚河汉界").action(async ({ session }) => {
463
486
  await session.execute(`cchess -h`);
464
487
  });
465
- 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) {
466
501
  const { username, userId, channelId } = session;
467
502
  await updateNameInPlayerRecord(userId, username);
468
503
  const gameRecord = await getGameRecord(channelId);
@@ -520,11 +555,10 @@ function apply(ctx, config) {
520
555
  title: "已入座,静候对手",
521
556
  at: username,
522
557
  body: [
523
- field("阵营", withSideIcon(selfSide)),
524
- field("当前人数", `${players.length} 人`),
558
+ field("阵营", withSideIcon(selfSide)) + " " + field("当前人数", `${players.length} 人`),
525
559
  !blackPlayers.length ? "黑方尚无人。" : "红方尚无人。"
526
560
  ],
527
- tips: ["好友发送「cchess.开始」补位,双方就位即自动开局", "「cchess.开始 黑」可指定阵营"]
561
+ tips: ["好友发送「cchess.开始」即可补位,指定阵营可发「cchess.开始 黑」"]
528
562
  }));
529
563
  }
530
564
  await ctx.database.set("cchess_game_records", { channelId }, { isStarted: true });
@@ -533,32 +567,46 @@ function apply(ctx, config) {
533
567
  icon: "✅",
534
568
  title: "楚河汉界,对局开始",
535
569
  body: [
536
- `${withSideIcon("红方")}(${redPlayers.length})`,
537
- ...redPlayers.map((player) => ` @${player.username}`),
538
- "",
539
- `${withSideIcon("黑方")}(${blackPlayers.length})`,
540
- ...blackPlayers.map((player) => ` @${player.username}`),
541
- "",
542
- field("先手", withSideIcon(sideString))
570
+ `${withSideIcon("红方")} · ${redPlayers.map((player) => `@${player.username}`).join("、")}`,
571
+ `${withSideIcon("黑方")} · ${blackPlayers.map((player) => `@${player.username}`).join("、")}`,
572
+ `先手 · ${withSideIcon(sideString)}`
543
573
  ],
544
574
  tips: ["直接发送着法即可落子,如「炮二平五」或「b2e2」"],
545
575
  image: await renderBoard(channelId)
546
576
  }));
547
- });
548
- ctx.command("cchess.结束", "结束当前对局").action(async ({ session }) => {
577
+ }
578
+ __name(startGame, "startGame");
579
+ ctx.command("cchess.结束", "结束正在进行的一局").userFields(["id", "name", "authority"]).action(async ({ session }) => {
549
580
  const { username, userId, channelId } = session;
550
581
  await updateNameInPlayerRecord(userId, username);
551
582
  const gameRecord = await getGameRecord(channelId);
552
583
  if (!gameRecord.isStarted) {
553
584
  return await sendMessage(session, panel({
554
- icon: "⚠️",
585
+ icon: "💡",
555
586
  title: "棋盘尚且空空",
556
587
  at: username,
557
588
  body: ["当前没有进行中的对局,无需收拾残局。"],
558
589
  tips: ["发送「cchess.开始」入座,再开一局"]
559
590
  }));
560
591
  }
561
- 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
+ }
562
610
  return await sendMessage(session, panel({
563
611
  icon: "✅",
564
612
  title: "对局已结束",
@@ -567,6 +615,9 @@ function apply(ctx, config) {
567
615
  tips: ["发送「cchess.开始」重整旗鼓"]
568
616
  }));
569
617
  });
618
+ ctx.command("cchess.落子 [move:string]", "发送着法,如「炮二平五」").usage("着法可用中式纵线记谱,也可用坐标,如 `b2e2`。").example("cchess.落子 炮二平五").action(async ({ session }, move) => {
619
+ return await handleMove(session, move);
620
+ });
570
621
  async function handleMove(session, moveOperation) {
571
622
  const { username, userId, channelId } = session;
572
623
  await updateNameInPlayerRecord(userId, username);
@@ -575,12 +626,7 @@ function apply(ctx, config) {
575
626
  return await sendMessage(session, invalidMovePanel(username, "看不懂这一着"));
576
627
  }
577
628
  if (busyChannels.has(channelId)) {
578
- return await sendMessage(session, panel({
579
- icon: "⏳",
580
- title: "棋局正在推演",
581
- at: username,
582
- body: ["上一着棋尚未结算,稍候片刻。"]
583
- }));
629
+ return await sendMessage(session, busyPanel(username));
584
630
  }
585
631
  busyChannels.add(channelId);
586
632
  try {
@@ -593,7 +639,7 @@ function apply(ctx, config) {
593
639
  icon: "⏳",
594
640
  title: "悔棋请求待决",
595
641
  at: username,
596
- body: ["对方的悔棋请求尚未答复,棋局暂歇。"],
642
+ body: ["对方的悔棋请求尚未答复,对局暂歇。"],
597
643
  tips: ["对方回复「同意」或「拒绝」即可表决"]
598
644
  }));
599
645
  }
@@ -679,6 +725,7 @@ function apply(ctx, config) {
679
725
  }
680
726
  const newMoves = record.moveList.slice(movesBefore);
681
727
  return await sendMessage(session, panel({
728
+ icon: "✅",
682
729
  title: `第 ${record.moveList.length} 手`,
683
730
  body: [
684
731
  ...newMoves.map((move) => `${withSideIcon(convertTurnToString(move.side))} ${move.chnMoveName} ${move.move}`),
@@ -691,7 +738,7 @@ function apply(ctx, config) {
691
738
  }
692
739
  }
693
740
  __name(handleMove, "handleMove");
694
- ctx.command("cchess.悔棋 [decision:string]", "请求悔棋或答复对方").usage("请求送出后,对方直接回复「同意」或「拒绝」即可表决,无需指令前缀。").action(async ({ session }, decision) => {
741
+ ctx.command("cchess.悔棋 [decision:string]", "请求悔棋或答复对方").usage("请求送出后,对方直接回复「同意」或「拒绝」即可表决,无需指令前缀;请求方发送「cchess.悔棋 拒绝」可撤下请求。").action(async ({ session }, decision) => {
695
742
  const { username, userId, channelId } = session;
696
743
  await updateNameInPlayerRecord(userId, username);
697
744
  if (decision) {
@@ -727,8 +774,8 @@ function apply(ctx, config) {
727
774
  }
728
775
  if (gameRecord.moveList.length < 1) {
729
776
  return await sendMessage(session, panel({
730
- icon: "⚠️",
731
- title: "棋局初开",
777
+ icon: "💡",
778
+ title: "对局初开",
732
779
  at: username,
733
780
  body: ["一子未落,无悔可言。"]
734
781
  }));
@@ -755,15 +802,32 @@ function apply(ctx, config) {
755
802
  }));
756
803
  }
757
804
  await ctx.database.set("cchess_game_records", { channelId }, { isRegretRequest: true });
805
+ const ticket = (regretTicket.get(channelId) ?? 0) + 1;
806
+ regretTicket.set(channelId, ticket);
807
+ ctx.setTimeout(async () => {
808
+ if (regretTicket.get(channelId) !== ticket) return;
809
+ const current = await getGameRecord(channelId);
810
+ if (!current?.isRegretRequest) return;
811
+ await ctx.database.set("cchess_game_records", { channelId }, { isRegretRequest: false });
812
+ await sendMessage(session, panel({
813
+ icon: "⏳",
814
+ title: "悔棋请求已作罢",
815
+ body: ["等了 30 秒没有等到答复,请求自动撤回。"],
816
+ tips: ["发送「cchess.悔棋」可以再提一次。"]
817
+ }));
818
+ }, 30 * 1e3);
758
819
  return await sendMessage(session, panel({
759
820
  icon: "⏳",
760
821
  title: "悔棋请求已送出",
761
822
  at: username,
762
- body: [field("等待答复", withSideIcon(sideString))],
763
- tips: ["对方回复「同意」或「拒绝」即可表决"]
823
+ body: [
824
+ field("等待答复", withSideIcon(sideString)),
825
+ "对方回复「同意」或「拒绝」即可表决,30 秒等不到答复会自动作废。"
826
+ ],
827
+ tips: ["发送「cchess.悔棋 拒绝」可撤下这次请求"]
764
828
  }));
765
829
  });
766
- ctx.command("cchess.认输", "认输并结束对局").action(async ({ session }) => {
830
+ ctx.command("cchess.认输", "认输并判对方获胜").action(async ({ session }) => {
767
831
  const { username, userId, channelId } = session;
768
832
  await updateNameInPlayerRecord(userId, username);
769
833
  const gameRecord = await getGameRecord(channelId);
@@ -823,7 +887,7 @@ function apply(ctx, config) {
823
887
  session,
824
888
  byLose ? "lose" : "win",
825
889
  byLose ? "总输场排行榜" : "总胜场排行榜",
826
- byLose ? "🍂" : "🏆",
890
+ "📋",
827
891
  size
828
892
  );
829
893
  }
@@ -842,7 +906,7 @@ function apply(ctx, config) {
842
906
  win: 0
843
907
  });
844
908
  return await sendMessage(session, panel({
845
- icon: "🔍",
909
+ icon: "📋",
846
910
  title: "棋士档案",
847
911
  at: session.username,
848
912
  body: [field("查询对象", username), "尚无对局记录,静待首战。"],
@@ -853,41 +917,39 @@ function apply(ctx, config) {
853
917
  const total = win + lose;
854
918
  const winRate = total === 0 ? "—" : `${(win / total * 100).toFixed(1)}%`;
855
919
  return await sendMessage(session, panel({
856
- icon: "🔍",
920
+ icon: "📋",
857
921
  title: "棋士档案",
858
922
  at: session.username,
859
923
  body: [
860
924
  field("查询对象", username),
861
- field("胜 场", `${win} 局`),
862
- field("负 场", `${lose} 局`),
863
- field("总对局", `${total} 局`),
864
- field("胜 率", winRate)
925
+ field("胜 场", `${win} 局`) + " " + field("负 场", `${lose} 局`),
926
+ field("总对局", `${total} 局`) + " " + field("胜 率", winRate)
865
927
  ]
866
928
  }));
867
929
  });
868
930
  function notStartedPanel(username) {
869
931
  return panel({
870
- icon: "⚠️",
871
- title: "棋局尚未开始",
932
+ icon: "💡",
933
+ title: "对局尚未开始",
872
934
  at: username,
873
935
  body: ["当前频道还没有进行中的对局。"],
874
- tips: ["「cchess.开始」入座并对弈", "「cchess.开始 人机」挑战皮卡鱼"]
936
+ tips: ["「cchess.开始」入座对弈 · 「cchess.开始 人机」挑战皮卡鱼"]
875
937
  });
876
938
  }
877
939
  __name(notStartedPanel, "notStartedPanel");
878
940
  function alreadyStartedPanel(username) {
879
941
  return panel({
880
- icon: "⚠️",
881
- title: "棋局已经开战",
942
+ icon: "💡",
943
+ title: "对局已经开战",
882
944
  at: username,
883
945
  body: ["本局尚未结束,不必重开。"],
884
- tips: ["「cchess.认输」结束本局", "「cchess.结束」强制清盘"]
946
+ tips: ["「cchess.认输」结束本局 · 「cchess.结束」清盘"]
885
947
  });
886
948
  }
887
949
  __name(alreadyStartedPanel, "alreadyStartedPanel");
888
950
  function notJoinedPanel(username) {
889
951
  return panel({
890
- icon: "⚠️",
952
+ icon: "💡",
891
953
  title: "你尚未入座",
892
954
  at: username,
893
955
  body: ["先入座,才好落子。"],
@@ -895,6 +957,15 @@ function apply(ctx, config) {
895
957
  });
896
958
  }
897
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");
898
969
  function analyzingPanel(username) {
899
970
  return panel({
900
971
  icon: "⏳",
@@ -906,7 +977,7 @@ function apply(ctx, config) {
906
977
  __name(analyzingPanel, "analyzingPanel");
907
978
  function notYourTurnPanel(username, yourSide, currentSide) {
908
979
  return panel({
909
- icon: "⚠️",
980
+ icon: "",
910
981
  title: "尚未轮到你",
911
982
  at: username,
912
983
  body: [field("你的阵营", withSideIcon(yourSide)), field("当前轮走", withSideIcon(currentSide))],
@@ -916,11 +987,11 @@ function apply(ctx, config) {
916
987
  __name(notYourTurnPanel, "notYourTurnPanel");
917
988
  function invalidMovePanel(username, reason) {
918
989
  return panel({
919
- icon: "",
990
+ icon: "⚠️",
920
991
  title: reason,
921
992
  at: username,
922
993
  body: ["请确认着法是否合乎棋规。"],
923
- 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"]
924
995
  });
925
996
  }
926
997
  __name(invalidMovePanel, "invalidMovePanel");
@@ -937,15 +1008,21 @@ function apply(ctx, config) {
937
1008
  function invalidLeaderboardSizePanel(username) {
938
1009
  return panel({
939
1010
  icon: "⚠️",
940
- title: "榜单人数有误",
1011
+ title: "排行榜人数有误",
941
1012
  at: username,
942
- body: ["请输入不小于 0 的整数。"],
943
- tips: ["例如:cchess.战绩 榜 10"]
1013
+ body: ["请发送不小于 0 的整数。"],
1014
+ tips: ["发送「cchess.战绩 榜 10」可查询前 10 名"]
944
1015
  });
945
1016
  }
946
1017
  __name(invalidLeaderboardSizePanel, "invalidLeaderboardSizePanel");
947
1018
  async function renderBoard(channelId) {
948
- return import_koishi.h.image(await drawChessBoard(channelId), imageMimeType).toString();
1019
+ if (config.disableImages) return void 0;
1020
+ try {
1021
+ return import_koishi.h.image(await drawChessBoard(channelId), imageMimeType).toString();
1022
+ } catch (error) {
1023
+ logger.warn("棋盘渲染失败,本次只发文本:%s", error.message);
1024
+ return void 0;
1025
+ }
949
1026
  }
950
1027
  __name(renderBoard, "renderBoard");
951
1028
  async function settleVictory(channelId, winSide, loseSide, title, options = {}) {
@@ -963,8 +1040,7 @@ function apply(ctx, config) {
963
1040
  at,
964
1041
  body: [
965
1042
  ...extra,
966
- field("胜方", withSideIcon(convertTurnToString(winSide))),
967
- field("棋手", winners.length ? winners.map((player) => `@${player.username}`).join("、") : "皮卡鱼")
1043
+ field("胜方", withSideIcon(convertTurnToString(winSide))) + " " + field("棋手", winners.length ? winners.map((player) => `@${player.username}`).join("、") : "皮卡鱼")
968
1044
  ],
969
1045
  tips: ["发送「cchess.开始」再战一局"],
970
1046
  image
@@ -1005,8 +1081,7 @@ function apply(ctx, config) {
1005
1081
  body: [
1006
1082
  field("棋 手", withSideIcon(humanSide)),
1007
1083
  field("皮卡鱼", withSideIcon(engineSide)),
1008
- field("先 手", withSideIcon(sideString)),
1009
- field("思考深度", `${thinkingDepth} 层`)
1084
+ field("先 手", withSideIcon(sideString)) + " " + field("思考深度", `${thinkingDepth} 层`)
1010
1085
  ],
1011
1086
  tips: ["直接发送着法即可落子,如「炮二平五」或「b2e2」"],
1012
1087
  image: await renderBoard(channelId)
@@ -1031,6 +1106,15 @@ function apply(ctx, config) {
1031
1106
  if (playerRecord.length === 0) return await sendMessage(session, notJoinedPanel(username));
1032
1107
  const sideString = convertTurnToString(gameRecord.turn);
1033
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
+ }
1034
1118
  return await sendMessage(session, panel({
1035
1119
  icon: "⚠️",
1036
1120
  title: "无权表决",
@@ -1044,7 +1128,7 @@ function apply(ctx, config) {
1044
1128
  icon: "⚠️",
1045
1129
  title: "悔棋被拒",
1046
1130
  at: username,
1047
- body: ["落子无悔,棋局继续。"],
1131
+ body: ["落子无悔,对局继续。"],
1048
1132
  image: await renderBoard(channelId)
1049
1133
  }));
1050
1134
  }
@@ -1109,8 +1193,8 @@ function apply(ctx, config) {
1109
1193
  return await sendMessage(session, panel({
1110
1194
  icon,
1111
1195
  title,
1112
- body: topPlayers.length ? topPlayers.map((player, index) => `${MEDALS[index] ?? `${index + 1}.`} ${player.username} ${player[sortField]} 局`) : ["榜上尚无名姓,快去手谈一局。"],
1113
- 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.开始」开启一局"]
1114
1198
  }));
1115
1199
  }
1116
1200
  __name(getLeaderboard, "getLeaderboard");
@@ -1632,23 +1716,25 @@ function apply(ctx, config) {
1632
1716
  const rowAt = /* @__PURE__ */ __name((row) => FRAME_BOARD.y + (row * CELL_WIDTH + BOARD_PADDING) * FRAME_ZOOM, "rowAt");
1633
1717
  context.textAlign = "center";
1634
1718
  context.textBaseline = "middle";
1635
- const label = /* @__PURE__ */ __name((text, x, y, size, color) => {
1636
- 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;
1637
1723
  context.fillStyle = color;
1638
1724
  context.fillText(text, x, y);
1639
1725
  }, "label");
1640
1726
  for (let col = 0; col < 9; col++) {
1641
1727
  const x = columnAt(isFlipBoard ? 8 - col : col);
1642
- label(FILES[col], x, 30, 22, FRAME.onSurfaceVariant);
1643
- label(String(col + 1), x, 64, 22, FRAME.onSurfaceVariant);
1644
- label(RED_FILES[col], x, FRAME_HEIGHT - 62, 24, FRAME.onSurfaceVariant);
1645
- 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);
1646
1732
  }
1647
1733
  for (let row = 0; row < 10; row++) {
1648
1734
  const y = rowAt(isFlipBoard ? 9 - row : row);
1649
1735
  const text = String(9 - row);
1650
- label(text, 19, y, 22, FRAME.onSurfaceVariant);
1651
- 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);
1652
1738
  }
1653
1739
  return canvas.toBuffer(imageMimeType);
1654
1740
  }
@@ -2132,7 +2218,7 @@ function apply(ctx, config) {
2132
2218
  }
2133
2219
  }
2134
2220
  __name(drawChessBoard, "drawChessBoard");
2135
- 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) {
2136
2222
  var angle = Math.atan2(toY - fromY, toX - fromX);
2137
2223
  var headAngle = 45 * Math.PI / 180;
2138
2224
  ctx2.beginPath();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-cchess",
3
3
  "description": "Koishi 的中国象棋插件。",
4
- "version": "2.0.1",
4
+ "version": "2.2.0",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [