UNPKG

koishi-plugin-gomoku

Version:

A Gomoku (Five in a Row) game plugin for Koishi

552 lines (551 loc) 21.5 kB
"use strict"; var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Config = void 0; exports.apply = apply; const koishi_1 = require("koishi"); const di_1 = require("@koishijs/di"); // 设置默认配置 exports.Config = koishi_1.Schema.object({ boardSize: koishi_1.Schema.number().default(15).description('棋盘大小'), winCondition: koishi_1.Schema.number().default(5).description('获胜条件(连子数)') }); // 棋子类型 var ChessPiece; (function (ChessPiece) { ChessPiece[ChessPiece["EMPTY"] = 0] = "EMPTY"; ChessPiece[ChessPiece["BLACK"] = 1] = "BLACK"; ChessPiece[ChessPiece["WHITE"] = 2] = "WHITE"; })(ChessPiece || (ChessPiece = {})); // 游戏状态 var GameState; (function (GameState) { GameState[GameState["WAITING"] = 0] = "WAITING"; GameState[GameState["PLAYING"] = 1] = "PLAYING"; GameState[GameState["FINISHED"] = 2] = "FINISHED"; })(GameState || (GameState = {})); // 游戏会话类 class GomokuGame { board; currentPlayer; players; state; currentTurn; lastMove; channelId; guildId; id; constructor(data, boardSize) { if (data.board) { // 从数据库恢复棋盘 this.board = JSON.parse(data.board); } else { // 创建新棋盘 this.board = Array(boardSize).fill(0).map(() => Array(boardSize).fill(ChessPiece.EMPTY)); } this.id = data.id || 0; this.channelId = data.channelId || ''; this.guildId = data.guildId || ''; this.currentPlayer = data.currentPlayer || ''; this.players = data.players ? (typeof data.players === 'string' ? JSON.parse(data.players) : data.players) : []; this.state = data.state !== undefined ? data.state : GameState.WAITING; this.currentTurn = data.currentTurn !== undefined ? data.currentTurn : ChessPiece.BLACK; if (data.lastMove) { this.lastMove = JSON.parse(data.lastMove); } else { this.lastMove = [-1, -1]; } } // 转换为数据库格式 toData() { return { id: this.id, channelId: this.channelId, guildId: this.guildId, board: JSON.stringify(this.board), currentPlayer: this.currentPlayer, players: this.players, state: this.state, currentTurn: this.currentTurn, lastMove: JSON.stringify(this.lastMove), updateTime: Date.now() }; } // 添加玩家 addPlayer(playerId) { if (this.players.length >= 2) return false; if (this.players.includes(playerId)) return false; this.players.push(playerId); // 如果是第二个玩家加入,开始游戏 if (this.players.length === 2) { this.state = GameState.PLAYING; this.currentPlayer = this.players[0]; } return true; } // 切换当前玩家 switchPlayer() { const currentIndex = this.players.indexOf(this.currentPlayer); const nextIndex = (currentIndex + 1) % this.players.length; this.currentPlayer = this.players[nextIndex]; this.currentTurn = this.currentTurn === ChessPiece.BLACK ? ChessPiece.WHITE : ChessPiece.BLACK; } // 检查坐标是否有效 isValidMove(row, col) { return row >= 0 && row < this.board.length && col >= 0 && col < this.board.length && this.board[row][col] === ChessPiece.EMPTY; } // 放置棋子 placeChess(row, col) { if (!this.isValidMove(row, col)) return false; this.board[row][col] = this.currentTurn; this.lastMove = [row, col]; return true; } // 检查是否获胜 checkWin(row, col, winCondition) { const directions = [ [0, 1], // 水平 [1, 0], // 垂直 [1, 1], // 对角线 [1, -1] // 反对角线 ]; const piece = this.board[row][col]; const size = this.board.length; for (const [dx, dy] of directions) { let count = 1; // 正向检查 for (let i = 1; i < winCondition; i++) { const newRow = row + i * dx; const newCol = col + i * dy; if (newRow < 0 || newRow >= size || newCol < 0 || newCol >= size || this.board[newRow][newCol] !== piece) { break; } count++; } // 反向检查 for (let i = 1; i < winCondition; i++) { const newRow = row - i * dx; const newCol = col - i * dy; if (newRow < 0 || newRow >= size || newCol < 0 || newCol >= size || this.board[newRow][newCol] !== piece) { break; } count++; } if (count >= winCondition) { return true; } } return false; } // 检查是否平局 checkDraw() { for (const row of this.board) { if (row.includes(ChessPiece.EMPTY)) { return false; } } return true; } // 重置游戏 reset(boardSize) { this.board = Array(boardSize).fill(0).map(() => Array(boardSize).fill(ChessPiece.EMPTY)); this.state = GameState.PLAYING; this.currentTurn = ChessPiece.BLACK; this.currentPlayer = this.players[0]; this.lastMove = [-1, -1]; } // 渲染棋盘 renderBoard() { const size = this.board.length; const letters = 'ABCDEFGHJKLMNOPQRSTUVWXYZ'.slice(0, size); let result = ' ' + letters.split('').join(' ') + '\n'; for (let i = 0; i < size; i++) { const rowNum = (i + 1).toString().padStart(2, ' '); let row = rowNum; for (let j = 0; j < size; j++) { if (this.lastMove[0] === i && this.lastMove[1] === j) { // 标记最后一步 switch (this.board[i][j]) { case ChessPiece.BLACK: row += '●'; break; case ChessPiece.WHITE: row += '○'; break; default: row += '+'; } } else { switch (this.board[i][j]) { case ChessPiece.BLACK: row += '●'; break; case ChessPiece.WHITE: row += '○'; break; default: row += '+'; } } if (j < size - 1) { row += ' '; } } result += row + '\n'; } return result; } } // 游戏管理器 class GomokuManager { ctx; config; database; constructor(ctx, config) { this.ctx = ctx; this.config = config; // 初始化数据库 ctx.model.extend('gomoku_games', { id: 'unsigned', channelId: 'string', guildId: 'string', board: 'text', currentPlayer: 'string', players: { type: 'json', initial: [] }, state: 'integer', currentTurn: 'integer', lastMove: 'text', createTime: 'integer', updateTime: 'integer', }, { primary: 'id', autoInc: true, }); } // 创建新游戏 async createGame(channelId, guildId, creatorId) { const existingGame = await this.getGameByChannel(channelId); if (existingGame && existingGame.state !== GameState.FINISHED) { throw new Error('当前频道已有游戏正在进行'); } const gameData = { channelId, guildId, currentPlayer: creatorId, players: [creatorId], state: GameState.WAITING, currentTurn: ChessPiece.BLACK, createTime: Date.now(), updateTime: Date.now(), }; const result = await this.ctx.database.create('gomoku_games', gameData); return new GomokuGame({ ...gameData, id: result.id }, this.config.boardSize); } // 获取频道中的游戏 async getGameByChannel(channelId) { const data = await this.ctx.database .select('gomoku_games') .where({ channelId }) .orderBy('id', 'desc') .limit(1) .execute(); if (!data.length) return null; return new GomokuGame(data[0], this.config.boardSize); } // 获取所有进行中的游戏 async getActiveGames() { const data = await this.ctx.database .select('gomoku_games') .where({ state: GameState.PLAYING }) .execute(); return data.map(game => new GomokuGame(game, this.config.boardSize)); } // 加入游戏 async joinGame(channelId, playerId) { const game = await this.getGameByChannel(channelId); if (!game) throw new Error('找不到游戏'); if (!game.addPlayer(playerId)) { throw new Error('无法加入游戏'); } await this.updateGame(game); return game; } // 更新游戏状态 async updateGame(game) { await this.ctx.database .set('gomoku_games', game.id, game.toData()); } // 处理玩家移动 async handleMove(channelId, playerId, row, col) { const game = await this.getGameByChannel(channelId); if (!game) return { success: false, message: '找不到游戏' }; if (game.state !== GameState.PLAYING) { return { success: false, message: '游戏尚未开始或已经结束' }; } if (game.currentPlayer !== playerId) { return { success: false, message: '现在不是你的回合' }; } if (!game.placeChess(row, col)) { return { success: false, message: '无效的落子位置' }; } // 检查胜负 if (game.checkWin(row, col, this.config.winCondition)) { game.state = GameState.FINISHED; await this.updateGame(game); return { success: true, win: true }; } // 检查平局 if (game.checkDraw()) { game.state = GameState.FINISHED; await this.updateGame(game); return { success: true, draw: true }; } // 切换玩家 game.switchPlayer(); await this.updateGame(game); return { success: true }; } // 重置游戏 async resetGame(channelId) { const game = await this.getGameByChannel(channelId); if (!game) throw new Error('找不到游戏'); game.reset(this.config.boardSize); await this.updateGame(game); return game; } // 结束游戏 async endGame(channelId) { const game = await this.getGameByChannel(channelId); if (!game) return; game.state = GameState.FINISHED; await this.updateGame(game); } } __decorate([ (0, di_1.Inject)(), __metadata("design:type", Object) ], GomokuManager.prototype, "database", void 0); // 插件主体 function apply(ctx, config) { // 创建游戏管理器 const manager = new GomokuManager(ctx, config); // 注册五子棋命令 const cmd = ctx.command('五子棋', '五子棋游戏'); // 创建游戏 cmd.subcommand('.创建', '创建新的五子棋游戏') .action(async ({ session }) => { if (!session?.channelId || !session?.guildId) return '此命令只能在群聊中使用'; try { if (!session.userId) return '无法获取用户ID'; const game = await manager.createGame(session.channelId, session.guildId, session.userId); return [ `${(0, koishi_1.h)('at', { id: session.userId })} 创建了一局五子棋游戏!`, '\n等待另一位玩家加入...', '\n使用 五子棋加入 来加入游戏' ]; } catch (e) { const errorMessage = e instanceof Error ? e.message : '未知错误'; return `创建游戏失败: ${errorMessage}`; } }); // 加入游戏 cmd.subcommand('.加入', '加入当前五子棋游戏') .action(async ({ session }) => { if (!session?.channelId) return '此命令只能在群聊中使用'; try { if (!session.userId) return '无法获取用户ID'; const game = await manager.joinGame(session.channelId, session.userId); if (game.state === GameState.PLAYING) { return [ `${(0, koishi_1.h)('at', { id: session.userId })} 加入了游戏!`, '\n游戏开始!', `\n${(0, koishi_1.h)('at', { id: game.players[0] })} (●) vs ${(0, koishi_1.h)('at', { id: game.players[1] })} (○)`, `\n${(0, koishi_1.h)('at', { id: game.currentPlayer })} 执黑先行`, '\n使用 五子棋落子 <位置> 下棋,例如: 五子棋落子 H8', `\n棋盘:\n${(0, koishi_1.h)('code', { lang: 'plain' }, game.renderBoard())}` ]; } else { return [ `${(0, koishi_1.h)('at', { id: session.userId })} 加入了游戏!`, '\n等待更多玩家加入...', '\n使用 五子棋加入 来加入游戏' ]; } } catch (e) { const errorMessage = e instanceof Error ? e.message : '未知错误'; return `加入游戏失败: ${errorMessage}`; } }); // 落子 cmd.subcommand('.落子 <position:string>', '在指定位置落子') .action(async ({ session }, position) => { if (!session?.channelId || !session?.userId) return '此命令只能在群聊中使用'; if (!position) return '请提供落子位置,例如: H8'; // 解析位置 position = position.toUpperCase(); const match = position.match(/^([A-Z])(\d{1,2})$/); if (!match) return '无效的位置格式,请使用字母+数字的格式,例如: H8'; const col = 'ABCDEFGHJKLMNOPQRSTUVWXYZ'.indexOf(match[1]); const row = parseInt(match[2]) - 1; if (col < 0 || col >= config.boardSize || row < 0 || row >= config.boardSize) { return `无效的位置,请提供在棋盘范围内的位置 (A1-${'ABCDEFGHJKLMNOPQRSTUVWXYZ'[config.boardSize - 1]}${config.boardSize})`; } try { const result = await manager.handleMove(session.channelId, session.userId, row, col); if (!result.success) { return result.message; } const game = await manager.getGameByChannel(session.channelId); if (result.win) { return [ `${(0, koishi_1.h)('at', { id: session.userId })} 获胜!`, `\n最后的棋盘:\n${game ? (0, koishi_1.h)('code', { lang: 'plain' }, game.renderBoard()) : '游戏不存在'}`, '\n游戏结束' ]; } if (result.draw) { return [ '游戏以平局结束!', `\n最后的棋盘:\n${game ? (0, koishi_1.h)('code', { lang: 'plain' }, game.renderBoard()) : '游戏不存在'}`, '\n游戏结束' ]; } return [ `${(0, koishi_1.h)('at', { id: session.userId })} 在 ${position} 落子`, `\n轮到 ${game ? (0, koishi_1.h)('at', { id: game.currentPlayer }) : '未知'} 了`, `\n棋盘:\n${game ? (0, koishi_1.h)('code', { lang: 'plain' }, game.renderBoard()) : '游戏不存在'}` ]; } catch (e) { const errorMessage = e instanceof Error ? e.message : '未知错误'; return `操作失败: ${errorMessage}`; } }); // 查看棋局 cmd.subcommand('.查看', '查看当前棋局') .action(async ({ session }) => { if (!session?.channelId) return '此命令只能在群聊中使用'; try { const game = await manager.getGameByChannel(session.channelId); if (!game) return '当前频道没有进行中的游戏'; let status = '等待中'; if (game.state === GameState.PLAYING) { status = '进行中'; } else if (game.state === GameState.FINISHED) { status = '已结束'; } return [ '当前游戏状态:' + status, `\n玩家:${game.players.map(id => (0, koishi_1.h)('at', { id })).join(' vs ')}`, game.state === GameState.PLAYING ? `\n轮到 ${(0, koishi_1.h)('at', { id: game.currentPlayer })} 行动` : '', `\n棋盘:\n${(0, koishi_1.h)('code', { lang: 'plain' }, game.renderBoard())}` ]; } catch (e) { const errorMessage = e instanceof Error ? e.message : '未知错误'; return `查看失败: ${errorMessage}`; } }); // 重置游戏 cmd.subcommand('.重置', '重置当前游戏') .action(async ({ session }) => { if (!session?.channelId) return '此命令只能在群聊中使用'; try { const game = await manager.getGameByChannel(session.channelId); if (!game) return '当前频道没有游戏'; if (!session.userId || !game.players.includes(session.userId)) { return '只有游戏参与者才能重置游戏'; } await manager.resetGame(session.channelId); return [ `${(0, koishi_1.h)('at', { id: session.userId })} 重置了游戏!`, '\n新的游戏开始了', `\n${(0, koishi_1.h)('at', { id: game.players[0] })} 执黑先行`, `\n棋盘:\n${(0, koishi_1.h)('code', { lang: 'plain' }, game.renderBoard())}` ]; } catch (e) { const errorMessage = e instanceof Error ? e.message : '未知错误'; return `重置游戏失败: ${errorMessage}`; } }); // 结束游戏 cmd.subcommand('.结束', '结束当前游戏') .action(async ({ session }) => { if (!session?.channelId) return '此命令只能在群聊中使用'; try { const game = await manager.getGameByChannel(session.channelId); if (!game) return '当前频道没有游戏'; if (!session.userId || !game.players.includes(session.userId)) { return '只有游戏参与者才能结束游戏'; } await manager.endGame(session.channelId); return [ `${(0, koishi_1.h)('at', { id: session.userId })} 结束了游戏!`, '\n游戏已经结束' ]; } catch (e) { const errorMessage = e instanceof Error ? e.message : '未知错误'; return `结束游戏失败: ${errorMessage}`; } }); // 查看所有进行中的游戏 cmd.subcommand('.列表', '查看所有进行中的游戏') .action(async ({ session }) => { try { const games = await manager.getActiveGames(); if (games.length === 0) { return '当前没有进行中的游戏'; } const gameList = games.map((game, index) => { const players = game.players.map(id => (0, koishi_1.h)('at', { id })).join(' vs '); return `${index + 1}. 频道 ${game.channelId}: ${players}`; }).join('\n'); return [ '进行中的游戏列表:', gameList ]; } catch (e) { const errorMessage = e instanceof Error ? e.message : '未知错误'; return `获取游戏列表失败: ${errorMessage}`; } }); }