diff --git a/README.md b/README.md index 6c4d8851..bf896e3f 100644 --- a/README.md +++ b/README.md @@ -261,7 +261,7 @@ in the appropriate channel's topic to use it. ## Commands -Total: 584 +Total: 585 ### Utility: @@ -613,6 +613,7 @@ Total: 584 * **balloon-pop:** Don't let yourself be the last one to pump the balloon before it pops! * **battle:** Engage in a turn-based battle against another user or the AI. * **bingo:** Play bingo with up to 99 other users. +* **chess:** Play a game of Chess with another user or the AI. * **connect-four:** Play a game of Connect Four with another user or the AI. * **dots-and-boxes:** Play a game of Dots and Boxes with another user. * **emoji-emoji-revolution:** Can you type arrow emoji faster than anyone else has ever typed them before? @@ -1057,6 +1058,8 @@ here. * axis-cult-sign-up ([Image](https://imgur.com/gallery/quQTD)) - [Cheng Xiao](https://www.instagram.com/chengxiao_0715/) * certificate (Signature) +- [Chessboard Image](https://chessboardimage.com/) + * chess (Piece Images) - [Christoph Mueller](https://www.fontsquirrel.com/fonts/list/foundry/christoph-mueller) * captcha ([Moms Typewriter Font](https://www.fontsquirrel.com/fonts/MomsTypewriter)) - [Chuck Norris](https://chucknorris.com/) @@ -1869,6 +1872,7 @@ here. * wikihow ([API](https://www.wikihow.com/api.php)) - [Wikimedia Commons](https://commons.wikimedia.org/wiki/Main_Page) * caution ([Image](https://commons.wikimedia.org/wiki/File:Caution_blank.svg)) + * chess ([Board Image](https://commons.wikimedia.org/wiki/File:Chess_board_blank.svg)) * danger ([Image](https://commons.wikimedia.org/wiki/File:Danger_blank.svg)) - [Wikipedia](https://www.wikipedia.org/) * fact ([API](https://en.wikipedia.org/w/api.php)) diff --git a/assets/images/chess/board.png b/assets/images/chess/board.png new file mode 100644 index 00000000..837f20c4 Binary files /dev/null and b/assets/images/chess/board.png differ diff --git a/assets/images/chess/pieces/black-bishop.png b/assets/images/chess/pieces/black-bishop.png new file mode 100644 index 00000000..5d13d1db Binary files /dev/null and b/assets/images/chess/pieces/black-bishop.png differ diff --git a/assets/images/chess/pieces/black-king.png b/assets/images/chess/pieces/black-king.png new file mode 100644 index 00000000..ec569dfb Binary files /dev/null and b/assets/images/chess/pieces/black-king.png differ diff --git a/assets/images/chess/pieces/black-knight.png b/assets/images/chess/pieces/black-knight.png new file mode 100644 index 00000000..f8a57484 Binary files /dev/null and b/assets/images/chess/pieces/black-knight.png differ diff --git a/assets/images/chess/pieces/black-pawn.png b/assets/images/chess/pieces/black-pawn.png new file mode 100644 index 00000000..c6b82f45 Binary files /dev/null and b/assets/images/chess/pieces/black-pawn.png differ diff --git a/assets/images/chess/pieces/black-queen.png b/assets/images/chess/pieces/black-queen.png new file mode 100644 index 00000000..3b0d270c Binary files /dev/null and b/assets/images/chess/pieces/black-queen.png differ diff --git a/assets/images/chess/pieces/black-rook.png b/assets/images/chess/pieces/black-rook.png new file mode 100644 index 00000000..ac3a260c Binary files /dev/null and b/assets/images/chess/pieces/black-rook.png differ diff --git a/assets/images/chess/pieces/white-bishop.png b/assets/images/chess/pieces/white-bishop.png new file mode 100644 index 00000000..109eefe7 Binary files /dev/null and b/assets/images/chess/pieces/white-bishop.png differ diff --git a/assets/images/chess/pieces/white-king.png b/assets/images/chess/pieces/white-king.png new file mode 100644 index 00000000..2f1ff667 Binary files /dev/null and b/assets/images/chess/pieces/white-king.png differ diff --git a/assets/images/chess/pieces/white-knight.png b/assets/images/chess/pieces/white-knight.png new file mode 100644 index 00000000..9b0cfeab Binary files /dev/null and b/assets/images/chess/pieces/white-knight.png differ diff --git a/assets/images/chess/pieces/white-pawn.png b/assets/images/chess/pieces/white-pawn.png new file mode 100644 index 00000000..b25af280 Binary files /dev/null and b/assets/images/chess/pieces/white-pawn.png differ diff --git a/assets/images/chess/pieces/white-queen.png b/assets/images/chess/pieces/white-queen.png new file mode 100644 index 00000000..ec6a25e9 Binary files /dev/null and b/assets/images/chess/pieces/white-queen.png differ diff --git a/assets/images/chess/pieces/white-rook.png b/assets/images/chess/pieces/white-rook.png new file mode 100644 index 00000000..512c4b33 Binary files /dev/null and b/assets/images/chess/pieces/white-rook.png differ diff --git a/commands/games-mp/chess.js b/commands/games-mp/chess.js new file mode 100644 index 00000000..b8bc1383 --- /dev/null +++ b/commands/games-mp/chess.js @@ -0,0 +1,214 @@ +const Command = require('../../structures/Command'); +const jsChess = require('js-chess-engine'); +const { createCanvas, loadImage } = require('canvas'); +const { stripIndents } = require('common-tags'); +const path = require('path'); +const { verify, reactIfAble } = require('../../util/Util'); +const { FAILURE_EMOJI_ID } = process.env; +const turnRegex = /^([A-H][1-8]) ?([A-H][1-8])$/; +const pieces = ['pawn', 'rook', 'knight', 'king', 'queen', 'bishop']; +const cols = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H']; + +module.exports = class ChessCommand extends Command { + constructor(client) { + super(client, { + name: 'chess', + group: 'games-mp', + memberName: 'chess', + description: 'Play a game of Chess with another user or the AI.', + credit: [ + { + name: 'Chessboard Image', + url: 'https://chessboardimage.com/', + reason: 'Piece Images' + }, + { + name: 'Wikimedia Commons', + url: 'https://commons.wikimedia.org/wiki/Main_Page', + reason: 'Board Image', + reasonURL: 'https://commons.wikimedia.org/wiki/File:Chess_board_blank.svg' + } + ], + args: [ + { + key: 'opponent', + prompt: 'What user would you like to challenge?', + type: 'user', + default: () => this.client.user + } + ] + }); + + this.images = null; + } + + async run(msg, { opponent }) { + if (opponent.id === msg.author.id) return msg.reply('You may not play against yourself.'); + const current = this.client.games.get(msg.channel.id); + if (current) return msg.reply(`Please wait until the current game of \`${current.name}\` is finished.`); + this.client.games.set(msg.channel.id, { name: this.name }); + if (!images) await this.loadImages(); + try { + if (!opponent.bot) { + await msg.say(`${opponent}, do you accept this challenge?`); + const verification = await verify(msg.channel, opponent); + if (!verification) { + this.client.games.delete(msg.channel.id); + return msg.say('Looks like they declined...'); + } + } + const game = new jsChess.Game(); + let lastTurnTimeout = false; + while (!game.exportJson().checkMate) { + const user = game.exportJson().turn === 'black' ? opponent : msg.author; + const gameState = game.exportJson(); + if (opponent.bot && !userTurn) { + game.aiMove(3); + } else { + await msg.say(stripIndents` + ${user}, what move do you want to make (ex. A1A2)? Type \`end\` to forfeit. + _You are ${gameState.check ? '**in check!**' : 'not in check.'}_ + `, { files: [{ attachment: this.displayBoard(gameState), name: 'chess.png' }] }); + const pickFilter = res => { + if (res.author.id !== user.id) return false; + const choice = res.content.toUpperCase(); + if (choice === 'END') return true; + const move = choice.match(turnRegex); + if (!move) return false; + const moves = game.moves(); + if (!moves[move[1]].includes(move[2])) { + reactIfAble(res, res.author, FAILURE_EMOJI_ID, '❌'); + return false; + } + return true; + }; + const turn = await msg.channel.awaitMessages(pickFilter, { + max: 1, + time: 60000 + }); + if (!turn.size) { + if (lastTurnTimeout) { + await msg.say('Game ended due to inactivity.'); + break; + } else { + await msg.say('Sorry, time is up! Playing random move.'); + const moves = game.moves(); + const pieces = Object.keys(moves); + const piece = pieces[Math.floor(Math.random() * pieces.length)]; + const move = moves[piece][Math.floor(Math.random() * moves[piece].length)]; + game.move(piece, move); + lastTurnTimeout = true; + continue; + } + } + const choice = turn.first().content.toUpperCase().match(turnRegex); + game.move(choice[1], choice[2]); + } + } + this.client.games.delete(msg.channel.id); + const gameState = game.exportJson(); + if (!gameState.checkMate) return msg.say('Game ended due to inactivity or forfeit.'); + const winner = gameState.turn === 'black' ? msg.author : opponent; + return msg.say(`Checkmate! Congrats, ${winner}!`, { + files: [{ attachment: this.displayBoard(gameState), name: 'chess.png' }] + }); + } catch (err) { + this.client.games.delete(msg.channel.id); + throw err; + } + } + + displayBoard(gameState) { + const canvas = createCanvas(this.images.board.width, this.images.board.height); + const ctx = canvas.getContext('2d'); + ctx.drawImage(this.images.board, 0, 0); + let w = 36; + let h = 40; + let row = 8; + let col = 0; + for (let i = 0; i < 64; i++) { + const piece = gameState.pieces[`${cols[col]}${row}`]; + if (!piece) continue; + const parsed = this.pickImage(piece); + ctx.drawImage(this.images[parsed.color][parsed.name], w, h, 52, 52); + w += 52 + 2; + col += 1; + if (i % 8 === 0 && i !== 0) { + w = 36; + col = 0; + h += 52 + 2; + row -= 1; + } + } + return canvas.toBuffer(); + } + + async loadImages() { + const images = { black: {}, white: {} }; + images.board = await loadImage(path.join(__dirname, '..', '..', 'assets', 'images', 'chess', 'board.png')); + for (const piece of pieces) { + const blk = `black-${piece}.png`; + images.black[piece] = await loadImage(path.join(__dirname, '..', '..', 'assets', 'images', 'chess', blk)); + const whi = `white-${piece}.png`; + images.white[piece] = await loadImage(path.join(__dirname, '..', '..', 'assets', 'images', 'chess', whi)); + } + this.images = images; + return images; + } + + pickImage(piece) { + let name; + let color; + switch (piece) { + case 'p': + name = 'pawn'; + color = 'black'; + break; + case 'n': + name = 'knight'; + color = 'black'; + break; + case 'b': + name = 'bishop'; + color = 'black'; + break; + case 'r': + name = 'rook'; + color = 'black'; + break; + case 'q': + name = 'queen'; + color = 'black'; + break; + case 'k': + name = 'king'; + color = 'black'; + break; + case 'P': + name = 'pawn'; + color = 'white'; + break; + case 'N': + name = 'knight'; + color = 'white'; + break; + case 'B': + name = 'bishop'; + color = 'white'; + break; + case 'R': + name = 'rook'; + color = 'white'; + break; + case 'Q': + name = 'queen'; + color = 'white'; + break; + case 'K': + name = 'king'; + color = 'white'; + break; + } + return { name, color }; + } +}; diff --git a/package.json b/package.json index 0db7e813..b5388b3c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "xiao", - "version": "126.11.3", + "version": "126.12.0", "description": "Your personal server companion.", "main": "Xiao.js", "scripts": { @@ -54,6 +54,7 @@ "html-entities": "^2.0.2", "ioredis": "^4.19.4", "js-beautify": "^1.13.4", + "js-chess-engine": "^0.6.0", "mathjs": "^9.0.0", "moment": "^2.29.1", "moment-duration-format": "^2.3.2",