diff --git a/commands/games-mp/chess.js.save b/commands/games-mp/chess.js.save new file mode 100644 index 00000000..29a75617 --- /dev/null +++ b/commands/games-mp/chess.js.save @@ -0,0 +1,231 @@ +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 { drawImageWithTint } = require('../../util/Canvas'); +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 (!this.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(); + this.client.games.get(msg.channel.id).data = game; + let lastTurnTimeout = false; + let prevGameState = null; + while (!game.exportJson().checkMate) { + const user = game.exportJson().turn === 'black' ? opponent : msg.author; + const gameState = game.exportJson(); + if (user.bot) { + prevGameState = Object.assign({}, game.exportJson()); + 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, prevGameState), name: 'chess.png' }] }); + prevGameState = Object.assign({}, game.exportJson()); + const moves = game.moves(); + 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; + if (!moves[move[1]] || !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) { + break; + } else { + await msg.say('Sorry, time is up! Playing random move.'); + const available = Object.keys(moves); + const piece = available[Math.floor(Math.random() * available.length)]; + const move = moves[piece][Math.floor(Math.random() * moves[piece].length)]; + game.move(piece, move); + lastTurnTimeout = true; + continue; + } + } + if (turn.first().content.toLowerCase() === 'end') break; + 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, prevGameState) 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}`]; + const prevGamePiece = prevGameState ? prevGameState.pieces[`${cols[col]}${row}`] : null; + if (prevGamePiece !== piece) console.log(piece, prevGamePiece); + if (piece) { + const parsed = this.pickImage(piece); + if (prevGameState && !prevGamePiece) { + console.log(true); + drawImageWithTint(ctx, this.images[parsed.color][parsed.name], 'green', w, h, 52, 52); + } else { + ctx.drawImage(this.images[parsed.color][parsed.name], w, h, 52, 52); + } + } else if (prevGameState && prevGamePiece) { + console.log(false); + ctx.fillStyle = 'green'; + ctx.globalAlpha = 0.5; + ctx.fillRect(w, h, 52, 52); + ctx.globalAlpha = 1; + } + w += 52 + 2; + col += 1; + if (col % 8 === 0 && col !== 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 5b0cd417..73cc903d 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "@vitalets/google-translate-api": "^4.0.0", "aki-api": "^5.2.1", "bombsweeper.js": "^1.0.1", + "bufferutil": "^4.0.3", "canvas": "^2.6.1", "cheerio": "^1.0.0-rc.5", "cloc": "^2.7.0", @@ -44,9 +45,10 @@ "custom-translate": "^2.2.8", "didyoumean2": "^4.1.0", "discord.js": "^12.5.1", - "discord.js-commando": "github:discordjs/Commando", + "discord.js-commando": "github:discordjs/commando", "dotenv": "^8.2.0", "emoji-regex": "^9.2.0", + "erlpack": "github:discord/erlpack", "eslint": "^7.18.0", "expr-eval": "^2.0.2", "gifencoder": "^2.0.1", @@ -67,10 +69,13 @@ "rss-parser": "^3.10.0", "semver": "^7.3.4", "sherlockjs": "^1.4.0", + "sodium": "^3.0.2", "stackblur-canvas": "^2.4.0", "tesseract.js": "^2.1.4", "tictactoe-minimax-ai": "^1.2.1", - "winston": "^3.3.3" + "utf-8-validate": "^5.0.4", + "winston": "^3.3.3", + "zlib-sync": "^0.1.7" }, "optionalDependencies": { "bufferutil": "^4.0.3", @@ -80,7 +85,7 @@ "zlib-sync": "^0.1.7" }, "devDependencies": { - "eslint-config-amber": "github:dragonfire535/eslint-config-amber", + "eslint-config-amber": "^2.0.4", "eslint-plugin-json": "^2.1.2" }, "eslintConfig": {