koishi-plugin-gomoku
Version:
A Gomoku (Five in a Row) game plugin for Koishi
552 lines (551 loc) • 21.5 kB
JavaScript
"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}`;
}
});
}