@zhin.js/plugin-tic-tac-toe 1.0.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.
Files changed (53) hide show
  1. package/README.md +36 -0
  2. package/lib/board-view.d.ts +24 -0
  3. package/lib/board-view.d.ts.map +1 -0
  4. package/lib/board-view.js +58 -0
  5. package/lib/board-view.js.map +1 -0
  6. package/lib/commands.d.ts +6 -0
  7. package/lib/commands.d.ts.map +1 -0
  8. package/lib/commands.js +111 -0
  9. package/lib/commands.js.map +1 -0
  10. package/lib/engine.d.ts +25 -0
  11. package/lib/engine.d.ts.map +1 -0
  12. package/lib/engine.js +117 -0
  13. package/lib/engine.js.map +1 -0
  14. package/lib/game-flow.d.ts +11 -0
  15. package/lib/game-flow.d.ts.map +1 -0
  16. package/lib/game-flow.js +143 -0
  17. package/lib/game-flow.js.map +1 -0
  18. package/lib/hub-register.d.ts +5 -0
  19. package/lib/hub-register.d.ts.map +1 -0
  20. package/lib/hub-register.js +30 -0
  21. package/lib/hub-register.js.map +1 -0
  22. package/lib/index.d.ts +2 -0
  23. package/lib/index.d.ts.map +1 -0
  24. package/lib/index.js +37 -0
  25. package/lib/index.js.map +1 -0
  26. package/lib/models.d.ts +55 -0
  27. package/lib/models.d.ts.map +1 -0
  28. package/lib/models.js +44 -0
  29. package/lib/models.js.map +1 -0
  30. package/lib/player-label.d.ts +16 -0
  31. package/lib/player-label.d.ts.map +1 -0
  32. package/lib/player-label.js +36 -0
  33. package/lib/player-label.js.map +1 -0
  34. package/lib/session-service.d.ts +50 -0
  35. package/lib/session-service.d.ts.map +1 -0
  36. package/lib/session-service.js +176 -0
  37. package/lib/session-service.js.map +1 -0
  38. package/lib/ttt-command.d.ts +5 -0
  39. package/lib/ttt-command.d.ts.map +1 -0
  40. package/lib/ttt-command.js +67 -0
  41. package/lib/ttt-command.js.map +1 -0
  42. package/package.json +54 -0
  43. package/plugin.yml +2 -0
  44. package/src/board-view.ts +93 -0
  45. package/src/commands.ts +129 -0
  46. package/src/engine.ts +131 -0
  47. package/src/game-flow.ts +217 -0
  48. package/src/hub-register.ts +31 -0
  49. package/src/index.ts +43 -0
  50. package/src/models.ts +104 -0
  51. package/src/player-label.ts +48 -0
  52. package/src/session-service.ts +205 -0
  53. package/src/ttt-command.ts +76 -0
package/README.md ADDED
@@ -0,0 +1,36 @@
1
+ # @zhin.js/plugin-tic-tac-toe
2
+
3
+ 跨平台井字棋插件,基于 Zhin.js **interactive 消息段**(按钮)与文本降级(1–9 落子)。
4
+
5
+ ## 安装
6
+
7
+ ```yaml
8
+ plugins:
9
+ - "@zhin.js/plugin-tic-tac-toe"
10
+ database:
11
+ dialect: sqlite
12
+ storage: ./data/zhin.db
13
+ ```
14
+
15
+ ## 命令
16
+
17
+ | 命令 | 说明 |
18
+ |------|------|
19
+ | `ttt` | 帮助与频道状态 |
20
+ | `ttt join` | 群聊排队,满 2 人开局 |
21
+ | `ttt leave` | 离开排队 |
22
+ | `ttt bot` | 人机对战 |
23
+ | `ttt quit` | 认输 |
24
+ | `ttt spectate` | 观战订阅 |
25
+
26
+ ## 交互
27
+
28
+ - **Native 按钮**:Sandbox、Telegram、Discord、KOOK、Lark、QQ 系等(见 `interactivePolicy: native`)
29
+ - **文本降级**:回复 `1`–`9` 落子(与按钮 `fallback.map` 一致)
30
+
31
+ ## 开发
32
+
33
+ ```bash
34
+ pnpm --filter @zhin.js/plugin-tic-tac-toe build
35
+ pnpm vitest run plugins/games/tic-tac-toe/tests
36
+ ```
@@ -0,0 +1,24 @@
1
+ import type { SendContent } from 'zhin.js';
2
+ import { channelKey } from '@zhin.js/game-shared';
3
+ import { type Board, type Cell } from './engine.js';
4
+ /** 游戏前缀(用于 payload) */
5
+ export declare const TTT_PREFIX = "ttt";
6
+ export { channelKey };
7
+ export declare function buildBoardInteractive(options: {
8
+ sessionId: string;
9
+ board: Board;
10
+ statusLine: string;
11
+ turnMark: Cell;
12
+ terminal?: boolean;
13
+ omitAsciiBoard?: boolean;
14
+ highlight?: number[];
15
+ }): SendContent;
16
+ export declare function buildFallbackMap(sessionId: string, board: Board): Record<string, string>;
17
+ export declare function parseTttPayload(payload: string): {
18
+ sessionId: string;
19
+ cell: number;
20
+ } | null;
21
+ /** QQ 等平台回调可能只带 button_id(如 c4),不含 ttt: 前缀 payload */
22
+ export declare function parseCellButtonId(value: string): number | null;
23
+ export declare function markName(mark: Cell): string;
24
+ //# sourceMappingURL=board-view.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"board-view.d.ts","sourceRoot":"","sources":["../src/board-view.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAW,WAAW,EAAE,MAAM,SAAS,CAAC;AACpD,OAAO,EAKL,UAAU,EAEX,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAGL,KAAK,KAAK,EACV,KAAK,IAAI,EAIV,MAAM,aAAa,CAAC;AAErB,uBAAuB;AACvB,eAAO,MAAM,UAAU,QAAQ,CAAC;AAEhC,OAAO,EAAE,UAAU,EAAE,CAAC;AAwBtB,wBAAgB,qBAAqB,CAAC,OAAO,EAAE;IAC7C,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,KAAK,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,IAAI,CAAC;IACf,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;CACtB,GAAG,WAAW,CAgBd;AAED,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAExF;AAED,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAK3F;AAED,sDAAsD;AACtD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAI9D;AAED,wBAAgB,QAAQ,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,CAE3C"}
@@ -0,0 +1,58 @@
1
+ import { buildGridKeyboard, buildGridFallbackMap, parseGridPayload, parseCellButtonId as parseButtonId, channelKey, } from '@zhin.js/game-shared';
2
+ import { asciiBoard, cellLabel, EMPTY, X, } from './engine.js';
3
+ /** 游戏前缀(用于 payload) */
4
+ export const TTT_PREFIX = 'ttt';
5
+ export { channelKey };
6
+ /** 将井字棋棋盘转为通用 GridCell 数组 */
7
+ function boardToCells(board, highlight) {
8
+ const highlightSet = new Set(highlight ?? []);
9
+ return board.map((cell, i) => ({
10
+ state: cell,
11
+ label: cell === EMPTY ? '·' : cellLabel(cell),
12
+ disabled: cell !== EMPTY,
13
+ highlight: highlightSet.has(i),
14
+ }));
15
+ }
16
+ /** 井字棋 ASCII 棋盘渲染器(适配 GridCell) */
17
+ function renderTttAscii(cells, rows, cols, highlight) {
18
+ const board = cells.map((c) => c.state);
19
+ return asciiBoard(board, highlight);
20
+ }
21
+ export function buildBoardInteractive(options) {
22
+ const { sessionId, board, statusLine, terminal, omitAsciiBoard, highlight } = options;
23
+ return buildGridKeyboard({
24
+ gamePrefix: TTT_PREFIX,
25
+ sessionId,
26
+ rows: 3,
27
+ cols: 3,
28
+ cells: boardToCells(board, highlight),
29
+ statusLine,
30
+ terminal,
31
+ omitAsciiBoard,
32
+ renderAscii: renderTttAscii,
33
+ highlight,
34
+ fallbackHint: '落子:回复数字 1-9(仅空格)',
35
+ });
36
+ }
37
+ export function buildFallbackMap(sessionId, board) {
38
+ return buildGridFallbackMap(TTT_PREFIX, sessionId, boardToCells(board));
39
+ }
40
+ export function parseTttPayload(payload) {
41
+ const result = parseGridPayload(payload, TTT_PREFIX);
42
+ if (!result)
43
+ return null;
44
+ if (result.cell > 8)
45
+ return null;
46
+ return { sessionId: result.sessionId, cell: result.cell };
47
+ }
48
+ /** QQ 等平台回调可能只带 button_id(如 c4),不含 ttt: 前缀 payload */
49
+ export function parseCellButtonId(value) {
50
+ const cell = parseButtonId(value);
51
+ if (cell === null || cell > 8)
52
+ return null;
53
+ return cell;
54
+ }
55
+ export function markName(mark) {
56
+ return mark === X ? '✕' : '○';
57
+ }
58
+ //# sourceMappingURL=board-view.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"board-view.js","sourceRoot":"","sources":["../src/board-view.ts"],"names":[],"mappings":"AACA,OAAO,EACL,iBAAiB,EACjB,oBAAoB,EACpB,gBAAgB,EAChB,iBAAiB,IAAI,aAAa,EAClC,UAAU,GAEX,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACL,UAAU,EACV,SAAS,EAGT,KAAK,EACL,CAAC,GAEF,MAAM,aAAa,CAAC;AAErB,uBAAuB;AACvB,MAAM,CAAC,MAAM,UAAU,GAAG,KAAK,CAAC;AAEhC,OAAO,EAAE,UAAU,EAAE,CAAC;AAEtB,6BAA6B;AAC7B,SAAS,YAAY,CAAC,KAAY,EAAE,SAAoB;IACtD,MAAM,YAAY,GAAG,IAAI,GAAG,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC;IAC9C,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC;QAC7B,KAAK,EAAE,IAAI;QACX,KAAK,EAAE,IAAI,KAAK,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC;QAC7C,QAAQ,EAAE,IAAI,KAAK,KAAK;QACxB,SAAS,EAAE,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC;KAC/B,CAAC,CAAC,CAAC;AACN,CAAC;AAED,mCAAmC;AACnC,SAAS,cAAc,CACrB,KAAuB,EACvB,IAAY,EACZ,IAAY,EACZ,SAAoB;IAEpB,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;IACxC,OAAO,UAAU,CAAC,KAAc,EAAE,SAAS,CAAC,CAAC;AAC/C,CAAC;AAED,MAAM,UAAU,qBAAqB,CAAC,OAQrC;IACC,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,cAAc,EAAE,SAAS,EAAE,GAAG,OAAO,CAAC;IAEtF,OAAO,iBAAiB,CAAC;QACvB,UAAU,EAAE,UAAU;QACtB,SAAS;QACT,IAAI,EAAE,CAAC;QACP,IAAI,EAAE,CAAC;QACP,KAAK,EAAE,YAAY,CAAC,KAAK,EAAE,SAAS,CAAC;QACrC,UAAU;QACV,QAAQ;QACR,cAAc;QACd,WAAW,EAAE,cAAc;QAC3B,SAAS;QACT,YAAY,EAAE,kBAAkB;KACjC,CAAC,CAAC;AACL,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,SAAiB,EAAE,KAAY;IAC9D,OAAO,oBAAoB,CAAC,UAAU,EAAE,SAAS,EAAE,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC;AAC1E,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,OAAe;IAC7C,MAAM,MAAM,GAAG,gBAAgB,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC;IACrD,IAAI,CAAC,MAAM;QAAE,OAAO,IAAI,CAAC;IACzB,IAAI,MAAM,CAAC,IAAI,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IACjC,OAAO,EAAE,SAAS,EAAE,MAAM,CAAC,SAAS,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,CAAC;AAC5D,CAAC;AAED,sDAAsD;AACtD,MAAM,UAAU,iBAAiB,CAAC,KAAa;IAC7C,MAAM,IAAI,GAAG,aAAa,CAAC,KAAK,CAAC,CAAC;IAClC,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IAC3C,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,IAAU;IACjC,OAAO,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC;AAChC,CAAC"}
@@ -0,0 +1,6 @@
1
+ import { type Plugin } from 'zhin.js';
2
+ import type { SessionServices } from './session-service.js';
3
+ export declare function registerCommands(plugin: Plugin, getServices: () => SessionServices | null): void;
4
+ export declare function registerInteractive(plugin: Plugin, getServices: () => SessionServices | null): void;
5
+ export declare function registerTextFallback(plugin: Plugin, getServices: () => SessionServices | null): void;
6
+ //# sourceMappingURL=commands.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../src/commands.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiD,KAAK,MAAM,EAAE,MAAM,SAAS,CAAC;AAMrF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAgE5D,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,eAAe,GAAG,IAAI,GACxC,IAAI,CAGN;AAgBD,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,eAAe,GAAG,IAAI,GAAG,IAAI,CAGnG;AAED,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,eAAe,GAAG,IAAI,GAAG,IAAI,CA+BpG"}
@@ -0,0 +1,111 @@
1
+ import { MessageCommand, getActionFromMessage } from 'zhin.js';
2
+ import { channelKey, normalizeTttAction } from '@zhin.js/game-shared';
3
+ import { buildFallbackMap } from './board-view.js';
4
+ import { parseBoard } from './engine.js';
5
+ import { handleMove } from './game-flow.js';
6
+ import { runTttCommand } from './ttt-command.js';
7
+ function actionPayload(message) {
8
+ return getActionFromMessage(message)?.payload;
9
+ }
10
+ /** @deprecated 使用 game-shared parseChoicePayload;保留 ttt 专用解析 */
11
+ function parseTttPayload(payload) {
12
+ const m = /^ttt:([^:]+):(\d)$/.exec(payload);
13
+ if (!m)
14
+ return null;
15
+ return { sessionId: m[1], cell: Number(m[2]) };
16
+ }
17
+ function parseCellButtonId(id) {
18
+ const m = /^c(\d)$/.exec(id);
19
+ if (!m)
20
+ return null;
21
+ const cell = Number(m[1]);
22
+ return cell >= 0 && cell <= 8 ? cell : null;
23
+ }
24
+ async function resolveTttMove(message, services) {
25
+ const action = getActionFromMessage(message);
26
+ if (!action)
27
+ return null;
28
+ const fromPayload = parseTttPayload(action.payload);
29
+ if (fromPayload)
30
+ return fromPayload;
31
+ const cell = parseCellButtonId(action.payload)
32
+ ?? parseCellButtonId(action.id ?? '');
33
+ if (cell == null)
34
+ return null;
35
+ const ch = channelKey(message);
36
+ const session = await services.session.getActiveForUser(ch, message.$sender.id)
37
+ ?? (action.sourceMessageId
38
+ ? await services.session.getActiveByBoardMessageId(action.sourceMessageId)
39
+ : null);
40
+ if (!session || session.channel_key !== ch)
41
+ return null;
42
+ return { sessionId: session.id, cell };
43
+ }
44
+ function registerTttPattern(plugin, pattern, desc, getServices) {
45
+ plugin.addCommand(new MessageCommand(pattern)
46
+ .desc(desc)
47
+ .action(async (message, result) => {
48
+ const services = getServices();
49
+ if (!services)
50
+ return '井字棋需要启用 database 配置。';
51
+ const raw = result.params.action ?? '';
52
+ const action = normalizeTttAction(raw);
53
+ return runTttCommand(plugin, services, message, action);
54
+ }));
55
+ }
56
+ export function registerCommands(plugin, getServices) {
57
+ registerTttPattern(plugin, 'ttt [action:word]', '井字棋(ttt)', getServices);
58
+ registerTttPattern(plugin, '井字棋 [action:word]', '井字棋(中文)', getServices);
59
+ }
60
+ async function handleTttAction(plugin, getServices, message) {
61
+ const services = getServices();
62
+ if (!services)
63
+ return false;
64
+ const move = await resolveTttMove(message, services);
65
+ if (!move)
66
+ return false;
67
+ const err = await handleMove(plugin, services, message, move.sessionId, move.cell);
68
+ if (err)
69
+ await message.$reply?.(err);
70
+ return true;
71
+ }
72
+ export function registerInteractive(plugin, getServices) {
73
+ plugin.registerInteractiveHandler('ttt:', (message) => handleTttAction(plugin, getServices, message));
74
+ plugin.registerInteractiveHandler('c', (message) => handleTttAction(plugin, getServices, message));
75
+ }
76
+ export function registerTextFallback(plugin, getServices) {
77
+ plugin.addMiddleware(async (message, next) => {
78
+ const services = getServices();
79
+ if (!services)
80
+ return next();
81
+ const inboundAction = actionPayload(message);
82
+ if (inboundAction?.startsWith('ttt:'))
83
+ return next();
84
+ const ch = channelKey(message);
85
+ const session = await services.session.getActiveForUser(ch, message.$sender.id);
86
+ if (!session)
87
+ return next();
88
+ const raw = message.$raw?.trim() ?? '';
89
+ let cell = null;
90
+ const direct = parseTttPayload(raw.startsWith('ttt:') ? raw : '');
91
+ if (direct && direct.sessionId === session.id) {
92
+ cell = direct.cell;
93
+ }
94
+ else {
95
+ const n = /^(\d)$/.exec(raw);
96
+ if (n) {
97
+ const map = buildFallbackMap(session.id, parseBoard(session.board));
98
+ const payload = map[n[1]];
99
+ const p = payload ? parseTttPayload(payload) : null;
100
+ if (p?.sessionId === session.id)
101
+ cell = p.cell;
102
+ }
103
+ }
104
+ if (cell == null)
105
+ return next();
106
+ const err = await handleMove(plugin, services, message, session.id, cell);
107
+ if (err)
108
+ await message.$reply?.(err);
109
+ });
110
+ }
111
+ //# sourceMappingURL=commands.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"commands.js","sourceRoot":"","sources":["../src/commands.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,25 @@
1
+ export declare const EMPTY = 0;
2
+ export declare const X = 1;
3
+ export declare const O = 2;
4
+ export type Cell = typeof EMPTY | typeof X | typeof O;
5
+ export type Board = Cell[];
6
+ export type GameStatus = 'active' | 'won' | 'draw' | 'aborted';
7
+ export declare function emptyBoard(): Board;
8
+ export declare function parseBoard(raw: string | number[] | null | undefined): Board;
9
+ export declare function cellLabel(cell: Cell): string;
10
+ export declare function asciiBoard(board: Board, highlight?: number[]): string;
11
+ export declare function checkWinner(board: Board): {
12
+ winner: Cell;
13
+ line: number[];
14
+ } | null;
15
+ export declare function isDraw(board: Board): boolean;
16
+ export declare function validMove(board: Board, cell: number): boolean;
17
+ export declare function applyMove(board: Board, cell: number, player: Cell): Board;
18
+ export declare function opponent(player: Cell): Cell;
19
+ /** Minimax for tic-tac-toe (O = AI by default when aiPlays O) */
20
+ export declare function bestMove(board: Board, ai?: Cell): number;
21
+ export declare function playerMark(playerId: string, session: {
22
+ playerX: string;
23
+ playerO: string;
24
+ }): Cell | null;
25
+ //# sourceMappingURL=engine.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,KAAK,IAAI,CAAC;AACvB,eAAO,MAAM,CAAC,IAAI,CAAC;AACnB,eAAO,MAAM,CAAC,IAAI,CAAC;AAEnB,MAAM,MAAM,IAAI,GAAG,OAAO,KAAK,GAAG,OAAO,CAAC,GAAG,OAAO,CAAC,CAAC;AACtD,MAAM,MAAM,KAAK,GAAG,IAAI,EAAE,CAAC;AAE3B,MAAM,MAAM,UAAU,GAAG,QAAQ,GAAG,KAAK,GAAG,MAAM,GAAG,SAAS,CAAC;AAQ/D,wBAAgB,UAAU,IAAI,KAAK,CAElC;AAED,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,IAAI,GAAG,SAAS,GAAG,KAAK,CAa3E;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,CAI5C;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,KAAK,EAAE,SAAS,CAAC,EAAE,MAAM,EAAE,GAAG,MAAM,CAYrE;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,KAAK,GAAG;IAAE,MAAM,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,EAAE,CAAA;CAAE,GAAG,IAAI,CASjF;AAED,wBAAgB,MAAM,CAAC,KAAK,EAAE,KAAK,GAAG,OAAO,CAE5C;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAE7D;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,GAAG,KAAK,CAKzE;AAED,wBAAgB,QAAQ,CAAC,MAAM,EAAE,IAAI,GAAG,IAAI,CAE3C;AAED,iEAAiE;AACjE,wBAAgB,QAAQ,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE,GAAE,IAAQ,GAAG,MAAM,CAe3D;AA0BD,wBAAgB,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,GAAG,IAAI,CAIvG"}
package/lib/engine.js ADDED
@@ -0,0 +1,117 @@
1
+ export const EMPTY = 0;
2
+ export const X = 1;
3
+ export const O = 2;
4
+ const WIN_LINES = [
5
+ [0, 1, 2], [3, 4, 5], [6, 7, 8],
6
+ [0, 3, 6], [1, 4, 7], [2, 5, 8],
7
+ [0, 4, 8], [2, 4, 6],
8
+ ];
9
+ export function emptyBoard() {
10
+ return Array(9).fill(EMPTY);
11
+ }
12
+ export function parseBoard(raw) {
13
+ if (Array.isArray(raw))
14
+ return raw.map((v) => Number(v));
15
+ if (typeof raw === 'string') {
16
+ try {
17
+ const parsed = JSON.parse(raw);
18
+ if (Array.isArray(parsed) && parsed.length === 9) {
19
+ return parsed.map((v) => Number(v));
20
+ }
21
+ }
22
+ catch {
23
+ // fall through
24
+ }
25
+ }
26
+ return emptyBoard();
27
+ }
28
+ export function cellLabel(cell) {
29
+ if (cell === X)
30
+ return '✕';
31
+ if (cell === O)
32
+ return '○';
33
+ return '·';
34
+ }
35
+ export function asciiBoard(board, highlight) {
36
+ const mark = (i) => {
37
+ const base = cellLabel(board[i]);
38
+ return highlight?.includes(i) ? `[${base}]` : ` ${base} `;
39
+ };
40
+ return [
41
+ `${mark(0)}|${mark(1)}|${mark(2)}`,
42
+ '-+-+-',
43
+ `${mark(3)}|${mark(4)}|${mark(5)}`,
44
+ '-+-+-',
45
+ `${mark(6)}|${mark(7)}|${mark(8)}`,
46
+ ].join('\n');
47
+ }
48
+ export function checkWinner(board) {
49
+ for (const line of WIN_LINES) {
50
+ const [a, b, c] = line;
51
+ const v = board[a];
52
+ if (v !== EMPTY && v === board[b] && v === board[c]) {
53
+ return { winner: v, line };
54
+ }
55
+ }
56
+ return null;
57
+ }
58
+ export function isDraw(board) {
59
+ return board.every((c) => c !== EMPTY) && !checkWinner(board);
60
+ }
61
+ export function validMove(board, cell) {
62
+ return Number.isInteger(cell) && cell >= 0 && cell < 9 && board[cell] === EMPTY;
63
+ }
64
+ export function applyMove(board, cell, player) {
65
+ if (!validMove(board, cell))
66
+ throw new Error('invalid move');
67
+ const next = [...board];
68
+ next[cell] = player;
69
+ return next;
70
+ }
71
+ export function opponent(player) {
72
+ return player === X ? O : X;
73
+ }
74
+ /** Minimax for tic-tac-toe (O = AI by default when aiPlays O) */
75
+ export function bestMove(board, ai = O) {
76
+ const human = opponent(ai);
77
+ let bestScore = -Infinity;
78
+ let move = -1;
79
+ for (let i = 0; i < 9; i++) {
80
+ if (board[i] !== EMPTY)
81
+ continue;
82
+ const next = applyMove(board, i, ai);
83
+ const score = minimax(next, 0, false, ai, human);
84
+ if (score > bestScore) {
85
+ bestScore = score;
86
+ move = i;
87
+ }
88
+ }
89
+ return move;
90
+ }
91
+ function minimax(board, depth, isAi, ai, human) {
92
+ const win = checkWinner(board);
93
+ if (win?.winner === ai)
94
+ return 10 - depth;
95
+ if (win?.winner === human)
96
+ return depth - 10;
97
+ if (isDraw(board))
98
+ return 0;
99
+ const player = isAi ? ai : human;
100
+ let best = isAi ? -Infinity : Infinity;
101
+ for (let i = 0; i < 9; i++) {
102
+ if (board[i] !== EMPTY)
103
+ continue;
104
+ const next = applyMove(board, i, player);
105
+ const score = minimax(next, depth + 1, !isAi, ai, human);
106
+ best = isAi ? Math.max(best, score) : Math.min(best, score);
107
+ }
108
+ return best;
109
+ }
110
+ export function playerMark(playerId, session) {
111
+ if (playerId === session.playerX)
112
+ return X;
113
+ if (playerId === session.playerO)
114
+ return O;
115
+ return null;
116
+ }
117
+ //# sourceMappingURL=engine.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engine.js","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ import type { Message, Plugin } from 'zhin.js';
2
+ import type { TttSessionRow } from './models.js';
3
+ import { BOT_ID } from './player-label.js';
4
+ import type { SessionServices, TttPlayerRef } from './session-service.js';
5
+ export { BOT_ID };
6
+ export declare function isBotSession(session: TttSessionRow): boolean;
7
+ export declare function sendOrEditBoard(plugin: Plugin, services: SessionServices, message: Message<any>, session: TttSessionRow, statusLine: string, terminal?: boolean, highlight?: number[]): Promise<string>;
8
+ export declare function handleMove(plugin: Plugin, services: SessionServices, message: Message<any>, sessionId: string, cell: number): Promise<string | null>;
9
+ export declare function startBotGame(plugin: Plugin, services: SessionServices, message: Message<any>): Promise<string>;
10
+ export declare function startPvpGame(plugin: Plugin, services: SessionServices, message: Message<any>, playerX: TttPlayerRef, playerO: TttPlayerRef): Promise<void>;
11
+ //# sourceMappingURL=game-flow.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"game-flow.d.ts","sourceRoot":"","sources":["../src/game-flow.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAW,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACxD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAejD,OAAO,EACL,MAAM,EAOP,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAE1E,OAAO,EAAE,MAAM,EAAE,CAAC;AAElB,wBAAgB,YAAY,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAE5D;AAED,wBAAsB,eAAe,CACnC,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,eAAe,EACzB,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC,EACrB,OAAO,EAAE,aAAa,EACtB,UAAU,EAAE,MAAM,EAClB,QAAQ,UAAQ,EAChB,SAAS,CAAC,EAAE,MAAM,EAAE,GACnB,OAAO,CAAC,MAAM,CAAC,CAkCjB;AAUD,wBAAsB,UAAU,CAC9B,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,eAAe,EACzB,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC,EACrB,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CA+DxB;AAqBD,wBAAsB,YAAY,CAChC,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,eAAe,EACzB,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC,GACpB,OAAO,CAAC,MAAM,CAAC,CAiBjB;AAED,wBAAsB,YAAY,CAChC,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,eAAe,EACzB,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC,EACrB,OAAO,EAAE,YAAY,EACrB,OAAO,EAAE,YAAY,GACpB,OAAO,CAAC,IAAI,CAAC,CAYf"}
@@ -0,0 +1,143 @@
1
+ import { buildBoardInteractive } from './board-view.js';
2
+ import { applyMove, bestMove, checkWinner, isDraw, parseBoard, playerMark, validMove, X, O, cellLabel, } from './engine.js';
3
+ import { BOT_ID, formatPlayerWithMark, formatRosterLine, formatTurnStatus, formatWinHeadline, playerRefForMark, senderDisplayName, } from './player-label.js';
4
+ export { BOT_ID };
5
+ export function isBotSession(session) {
6
+ return session.player_o === BOT_ID || session.player_x === BOT_ID;
7
+ }
8
+ export async function sendOrEditBoard(plugin, services, message, session, statusLine, terminal = false, highlight) {
9
+ const board = parseBoard(session.board);
10
+ const content = buildBoardInteractive({
11
+ sessionId: session.id,
12
+ board,
13
+ statusLine,
14
+ turnMark: session.turn,
15
+ terminal,
16
+ omitAsciiBoard: message.$adapter === 'qq',
17
+ highlight,
18
+ });
19
+ const adapter = plugin.root.inject(message.$adapter);
20
+ if (session.board_message_id) {
21
+ const msgId = await adapter.editMessage({
22
+ messageId: session.board_message_id,
23
+ context: String(message.$adapter),
24
+ endpoint: message.$endpoint,
25
+ id: message.$channel.id,
26
+ type: message.$channel.type,
27
+ content,
28
+ });
29
+ if (msgId !== session.board_message_id) {
30
+ await services.session.updateSession(session.id, { board_message_id: msgId });
31
+ }
32
+ return msgId;
33
+ }
34
+ const msgId = await message.$reply?.(content);
35
+ if (msgId) {
36
+ await services.session.updateSession(session.id, { board_message_id: msgId });
37
+ }
38
+ return msgId ?? '';
39
+ }
40
+ function turnCell(session) {
41
+ return session.turn === 1 ? X : O;
42
+ }
43
+ function playerIdForTurn(session) {
44
+ return session.turn === 1 ? session.player_x : session.player_o;
45
+ }
46
+ export async function handleMove(plugin, services, message, sessionId, cell) {
47
+ const session = await services.session.getById(sessionId);
48
+ if (!session || session.status !== 'active') {
49
+ return '对局不存在或已结束。';
50
+ }
51
+ if (session.channel_key !== `${message.$adapter}-${message.$endpoint}-${message.$channel.type}:${message.$channel.id}`) {
52
+ return '请在开局频道落子。';
53
+ }
54
+ const mark = playerMark(message.$sender.id, {
55
+ playerX: session.player_x,
56
+ playerO: session.player_o,
57
+ });
58
+ if (!mark) {
59
+ return '你不是本局玩家。';
60
+ }
61
+ if (mark !== turnCell(session)) {
62
+ const turn = turnCell(session);
63
+ const { id, name } = playerRefForMark(session, turn);
64
+ return `还没轮到你(当前轮到 ${formatPlayerWithMark(id, name, cellLabel(turn))})。`;
65
+ }
66
+ const board = parseBoard(session.board);
67
+ if (!validMove(board, cell)) {
68
+ return '该位置不可落子。';
69
+ }
70
+ const nextBoard = applyMove(board, cell, mark);
71
+ const win = checkWinner(nextBoard);
72
+ const draw = !win && isDraw(nextBoard);
73
+ const moveCount = session.move_count + 1;
74
+ await services.session.recordMove(session.id, message.$sender.id, cell, moveCount);
75
+ await services.session.updateSession(session.id, {
76
+ board: JSON.stringify(nextBoard),
77
+ move_count: moveCount,
78
+ turn: session.turn === 1 ? 2 : 1,
79
+ status: win ? 'won' : draw ? 'draw' : 'active',
80
+ winner: win ? win.winner : 0,
81
+ });
82
+ const updated = (await services.session.getById(session.id));
83
+ if (win) {
84
+ const status = formatWinHeadline(updated, win.winner);
85
+ await sendOrEditBoard(plugin, services, message, updated, status, true, win.line);
86
+ return null;
87
+ }
88
+ if (draw) {
89
+ await sendOrEditBoard(plugin, services, message, updated, '平局。', true);
90
+ return null;
91
+ }
92
+ // 人机:玩家落子后立刻由服务端代下,避免先发「轮到机器人」再发终盘(QQ 被动消息多耗一次)
93
+ if (isBotSession(updated) && playerIdForTurn(updated) === BOT_ID) {
94
+ await runBotMove(plugin, services, message, updated);
95
+ return null;
96
+ }
97
+ const status = formatTurnStatus(updated, moveCount);
98
+ await sendOrEditBoard(plugin, services, message, updated, status, false);
99
+ return null;
100
+ }
101
+ async function runBotMove(plugin, services, message, session) {
102
+ const board = parseBoard(session.board);
103
+ const aiMark = session.player_o === BOT_ID ? O : X;
104
+ const cell = bestMove(board, aiMark);
105
+ if (cell < 0)
106
+ return;
107
+ const fakeMessage = {
108
+ ...message,
109
+ $sender: { ...message.$sender, id: BOT_ID, name: '机器人' },
110
+ };
111
+ await handleMove(plugin, services, fakeMessage, session.id, cell);
112
+ }
113
+ export async function startBotGame(plugin, services, message) {
114
+ const ch = `${message.$adapter}-${message.$endpoint}-${message.$channel.type}:${message.$channel.id}`;
115
+ const active = await services.session.getActiveByChannel(ch);
116
+ if (active)
117
+ return '当前频道已有进行中的对局。';
118
+ const session = await services.session.createSession({
119
+ message,
120
+ playerX: message.$sender.id,
121
+ playerO: BOT_ID,
122
+ playerXName: senderDisplayName(message.$sender),
123
+ playerOName: '机器人',
124
+ boardJson: JSON.stringify([0, 0, 0, 0, 0, 0, 0, 0, 0]),
125
+ });
126
+ const status = `${formatRosterLine(session)} · 你先手 (✕)`;
127
+ await sendOrEditBoard(plugin, services, message, session, status, false);
128
+ return '开局成功!点击棋盘或回复数字落子。';
129
+ }
130
+ export async function startPvpGame(plugin, services, message, playerX, playerO) {
131
+ const session = await services.session.createSession({
132
+ message,
133
+ playerX: playerX.id,
134
+ playerO: playerO.id,
135
+ playerXName: playerX.displayName,
136
+ playerOName: playerO.displayName,
137
+ boardJson: JSON.stringify([0, 0, 0, 0, 0, 0, 0, 0, 0]),
138
+ });
139
+ const opener = formatPlayerWithMark(session.player_x, session.player_x_name, '✕');
140
+ const status = `${formatRosterLine(session)} · 先手:${opener}`;
141
+ await sendOrEditBoard(plugin, services, message, session, status, false);
142
+ }
143
+ //# sourceMappingURL=game-flow.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"game-flow.js","sourceRoot":"","sources":["../src/game-flow.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ import { TTT_HELP } from './ttt-command.js';
2
+ import type { SessionServices } from './session-service.js';
3
+ export declare function registerTttHub(getServices: () => SessionServices | null): () => void;
4
+ export { TTT_HELP };
5
+ //# sourceMappingURL=hub-register.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hub-register.d.ts","sourceRoot":"","sources":["../src/hub-register.ts"],"names":[],"mappings":"AAEA,OAAO,EAAiB,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC3D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAE5D,wBAAgB,cAAc,CAAC,WAAW,EAAE,MAAM,eAAe,GAAG,IAAI,GAAG,MAAM,IAAI,CAuBpF;AAED,OAAO,EAAE,QAAQ,EAAE,CAAC"}
@@ -0,0 +1,30 @@
1
+ import { getPlugin } from 'zhin.js';
2
+ import { ensureGameHubService } from '@zhin.js/game-shared';
3
+ import { runTttCommand, TTT_HELP } from './ttt-command.js';
4
+ export function registerTttHub(getServices) {
5
+ const plugin = getPlugin();
6
+ ensureGameHubService(plugin);
7
+ return plugin.registerGame({
8
+ id: 'ttt',
9
+ title: '井字棋',
10
+ icon: '♟️',
11
+ description: '三子连珠,群聊排队或人机对战',
12
+ commandPrefix: '井字棋',
13
+ quickStart: '人机',
14
+ aliases: ['ttt'],
15
+ menus: [
16
+ { id: 'bot', label: '🤖 人机对战', style: 'primary' },
17
+ { id: 'join', label: '👥 加入排队', groupOnly: true },
18
+ { id: 'spectate', label: '👀 观战', groupOnly: true },
19
+ { id: 'help', label: '📖 玩法说明' },
20
+ ],
21
+ runAction: async (actionId, ctx) => {
22
+ const services = getServices();
23
+ if (!services)
24
+ return '井字棋需要启用 database 配置。';
25
+ return runTttCommand(ctx.plugin, services, ctx.message, actionId);
26
+ },
27
+ });
28
+ }
29
+ export { TTT_HELP };
30
+ //# sourceMappingURL=hub-register.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hub-register.js","sourceRoot":"","sources":["../src/hub-register.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAC5D,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAG3D,MAAM,UAAU,cAAc,CAAC,WAAyC;IACtE,MAAM,MAAM,GAAG,SAAS,EAAE,CAAC;IAC3B,oBAAoB,CAAC,MAAM,CAAC,CAAC;IAC7B,OAAO,MAAM,CAAC,YAAY,CAAC;QACzB,EAAE,EAAE,KAAK;QACT,KAAK,EAAE,KAAK;QACZ,IAAI,EAAE,IAAI;QACV,WAAW,EAAE,gBAAgB;QAC7B,aAAa,EAAE,KAAK;QACpB,UAAU,EAAE,IAAI;QAChB,OAAO,EAAE,CAAC,KAAK,CAAC;QAChB,KAAK,EAAE;YACL,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,EAAE;YACjD,EAAE,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,IAAI,EAAE;YACjD,EAAE,EAAE,EAAE,UAAU,EAAE,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE;YACnD,EAAE,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE;SACjC;QACD,SAAS,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,EAAE,EAAE;YACjC,MAAM,QAAQ,GAAG,WAAW,EAAE,CAAC;YAC/B,IAAI,CAAC,QAAQ;gBAAE,OAAO,sBAAsB,CAAC;YAC7C,OAAO,aAAa,CAAC,GAAG,CAAC,MAAM,EAAE,QAAQ,EAAE,GAAG,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;QACpE,CAAC;KACF,CAAC,CAAC;AACL,CAAC;AAED,OAAO,EAAE,QAAQ,EAAE,CAAC"}
package/lib/index.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=index.d.ts.map