├── .gitignore ├── .env.example ├── resources ├── black │ ├── king.png │ ├── pawn.png │ ├── queen.png │ ├── rook.png │ ├── bishop.png │ └── knight.png ├── chessboard.png └── white │ ├── king.png │ ├── pawn.png │ ├── queen.png │ ├── rook.png │ ├── bishop.png │ └── knight.png ├── requirements.txt ├── model.py ├── LICENSE ├── generator.py ├── README.md └── bot.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | venv 3 | .vscode/ 4 | .env -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | TOKEN= 2 | 3 | DATABASE_HOST= 4 | DATABASE_PORT= 5 | DATABASE_NAME= -------------------------------------------------------------------------------- /resources/black/king.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Davi0k/Chess-Bot/HEAD/resources/black/king.png -------------------------------------------------------------------------------- /resources/black/pawn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Davi0k/Chess-Bot/HEAD/resources/black/pawn.png -------------------------------------------------------------------------------- /resources/black/queen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Davi0k/Chess-Bot/HEAD/resources/black/queen.png -------------------------------------------------------------------------------- /resources/black/rook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Davi0k/Chess-Bot/HEAD/resources/black/rook.png -------------------------------------------------------------------------------- /resources/chessboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Davi0k/Chess-Bot/HEAD/resources/chessboard.png -------------------------------------------------------------------------------- /resources/white/king.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Davi0k/Chess-Bot/HEAD/resources/white/king.png -------------------------------------------------------------------------------- /resources/white/pawn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Davi0k/Chess-Bot/HEAD/resources/white/pawn.png -------------------------------------------------------------------------------- /resources/white/queen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Davi0k/Chess-Bot/HEAD/resources/white/queen.png -------------------------------------------------------------------------------- /resources/white/rook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Davi0k/Chess-Bot/HEAD/resources/white/rook.png -------------------------------------------------------------------------------- /resources/black/bishop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Davi0k/Chess-Bot/HEAD/resources/black/bishop.png -------------------------------------------------------------------------------- /resources/black/knight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Davi0k/Chess-Bot/HEAD/resources/black/knight.png -------------------------------------------------------------------------------- /resources/white/bishop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Davi0k/Chess-Bot/HEAD/resources/white/bishop.png -------------------------------------------------------------------------------- /resources/white/knight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Davi0k/Chess-Bot/HEAD/resources/white/knight.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Pillow==8.1.0 2 | python-dotenv==0.15.0 3 | shortuuid==1.0.1 4 | mongoengine==0.22.1 5 | discord.py==1.6.0 6 | chess==1.4.0 7 | -------------------------------------------------------------------------------- /model.py: -------------------------------------------------------------------------------- 1 | import os 2 | from mongoengine import connect, Document, LongField, IntField, DateTimeField 3 | from dotenv import load_dotenv 4 | load_dotenv() 5 | 6 | DATABASE_HOST = os.getenv("DATABASE_HOST") 7 | DATABASE_PORT = os.getenv("DATABASE_PORT") 8 | DATABASE_NAME = os.getenv("DATABASE_NAME") 9 | 10 | connect(DATABASE_NAME, port = int(DATABASE_PORT), host = DATABASE_HOST) 11 | 12 | class Statistics(Document): 13 | player = LongField(primary_key = True, required = True) 14 | totals = IntField(default = 0, required = False) 15 | wins = IntField(default = 0, required = False) 16 | losts = IntField(default = 0, required = False) 17 | timestamp = DateTimeField(required = False) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Davide Casale 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /generator.py: -------------------------------------------------------------------------------- 1 | from PIL import Image 2 | from typing import Union 3 | 4 | import chess 5 | 6 | class Generator: 7 | layout = [ 8 | [ chess.A8, chess.B8, chess.C8, chess.D8, chess.E8, chess.F8, chess.G8, chess.H8 ], 9 | [ chess.A7, chess.B7, chess.C7, chess.D7, chess.E7, chess.F7, chess.G7, chess.H7 ], 10 | [ chess.A6, chess.B6, chess.C6, chess.D6, chess.E6, chess.F6, chess.G6, chess.H6 ], 11 | [ chess.A5, chess.B5, chess.C5, chess.D5, chess.E5, chess.F5, chess.G5, chess.H5 ], 12 | [ chess.A4, chess.B4, chess.C4, chess.D4, chess.E4, chess.F4, chess.G4, chess.H4 ], 13 | [ chess.A3, chess.B3, chess.C3, chess.D3, chess.E3, chess.F3, chess.G3, chess.H3 ], 14 | [ chess.A2, chess.B2, chess.C2, chess.D2, chess.E2, chess.F2, chess.G2, chess.H2 ], 15 | [ chess.A1, chess.B1, chess.C1, chess.D1, chess.E1, chess.F1, chess.G1, chess.H1 ] 16 | ] 17 | 18 | coordinates = [ 25, 109, 194, 279, 363, 448, 531, 616 ] 19 | 20 | @staticmethod 21 | def generate(board: chess.BaseBoard) -> Image: 22 | chessboard = Image.open("resources/chessboard.png") 23 | 24 | for y, Y in enumerate(Generator.coordinates): 25 | for x, X in enumerate(Generator.coordinates): 26 | piece = board.piece_at(Generator.layout[y][x]) 27 | 28 | if piece is not None: 29 | path = Generator.path(piece) 30 | else: continue 31 | 32 | piece = Image.open(path).convert("RGBA") 33 | 34 | chessboard.paste(piece, (X, Y), piece) 35 | 36 | return chessboard 37 | 38 | @staticmethod 39 | def path(piece: chess.Piece) -> Union[str, None]: 40 | path = "resources/" 41 | 42 | if piece.color == chess.WHITE: path += "white/" 43 | if piece.color == chess.BLACK: path += "black/" 44 | 45 | if piece.piece_type == chess.PAWN: path += "pawn.png" 46 | if piece.piece_type == chess.KNIGHT: path += "knight.png" 47 | if piece.piece_type == chess.BISHOP: path += "bishop.png" 48 | if piece.piece_type == chess.ROOK: path += "rook.png" 49 | if piece.piece_type == chess.QUEEN: path += "queen.png" 50 | if piece.piece_type == chess.KING: path += "king.png" 51 | 52 | return path -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Chess-Bot By Davi0k 2 | Chess-Bot is a [Discord](https://discordapp.com/) BOT used to play chess directly in text channels against other users. It includes also a graphical chess board and a simple statistics database for each player. 3 | 4 | It makes use of the `python-chess` library as a chess engine. You can find it comfortably [here](https://python-chess.readthedocs.io/en/latest/). 5 | 6 | ## Starting the BOT 7 | First, create a copy of `.env.example` and install all of the needed PIP packages using the `requirements.txt` file: 8 | ``` 9 | cp .env.example .env 10 | python -m pip install -r requirements.txt 11 | ``` 12 | Now, open the newly created `.env` file and set-up these environment variables: 13 | 14 | `TOKEN`: The token of the BOT generated in [the developer portal](https://discordapp.com/developers/applications); 15 | 16 | `DATABASE_HOST`: The hostname of the dedicated MongoDB database; 17 | `DATABASE_PORT`: The port of the dedicated MongoDB database; 18 | `DATABASE_NAME`: The name that will be used for the database; 19 | 20 | Finally, you can run the BOT very easy: 21 | ``` 22 | python bot.py 23 | ``` 24 | 25 | ## Requirements for correct functioning 26 | In order to function correctly, the BOT needs the following active privileges within the Server that will host it: 27 | * `View Channels` permission: Necessary to allow the BOT to be able to view all text channels where users can play; 28 | * `Send Messages` permission: Necessary to allow the BOT to send messages relating to invitations, games and statistics within the text channels; 29 | * `Attach Files` permission: Necessary to allow the bot to send images containing the visual and current status of the chessboard within the text channels; 30 | 31 | Also, to get the best possible experience, you can activate the following Intent: 32 | * `Server Members Intent`: It allows the BOT to view the entire list of members connected to the Server that contains it, and not just that of users connected to any voice channel; 33 | 34 | ## Commands available within Chess-Bot 35 | Several commands are available in Chess-Bot. They are divided into two categories: Main and Chess. 36 | 37 | ### Main Category: 38 | The Main Category contains some useful and utility-based commands to simplify and extend the user experience. 39 | * `!rnd|random [minimum - optional] [maximum - optional]`: Draws a random number between `minimum` and `maximum`. If `minimum` and `maximum` are omitted, a number from 0 to 100 will be drawn; 40 | * `!coin|flip|coinflip`: Flips a coin and returns the result; 41 | * `!about|whois|info [user - optional]`: Shows different kind of informations about a specified `user`. If `user` is omitted, the context user info will be shown. 42 | 43 | ### Chess Category: 44 | The Chess Category contains all the main and unique commands of Chess-Bot to be able to play chess against your friends, or perhaps your enemies... 45 | * `!chess new|create|invite [user]`: Sends an invite for a new match to a specified `user`; 46 | * `!chess accept [user]`: Accepts an invite sent by a specified `user` and starts a new match; 47 | * `!chess invites`: Shows every out-coming and in-coming invites for the context user; 48 | * `!chess move|execute [initial] [final]`: Takes an `initial` and a `final` chess coordinate and executes a move in the current match; 49 | * `!chess show|chessboard`: Shows the current match chessboard disposition; 50 | * `!chess surrend`: Surrend and lost the current match; 51 | * `!chess statistics|stats [user - optional]`: Shows the statistics of a Discord user who played at least one match. If `user` is omitted, the context user stats will be shown. 52 | 53 | ## Some screen-shots about the BOT 54 | ![](https://i.ibb.co/BVcMNDj/Help.png) 55 | 56 | ![](https://i.ibb.co/hgks3Vp/Commands.png) 57 | 58 | ![](https://i.ibb.co/vv6RKHY/Start.png) 59 | 60 | ## License 61 | This project is released under the `MIT License`. You can find the original license source here: [https://opensource.org/licenses/MIT](https://opensource.org/licenses/MIT). 62 | 63 | ``` 64 | MIT License 65 | 66 | Copyright (c) 2020 Davide Casale 67 | 68 | Permission is hereby granted, free of charge, to any person obtaining a copy 69 | of this software and associated documentation files (the "Software"), to deal 70 | in the Software without restriction, including without limitation the rights 71 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 72 | copies of the Software, and to permit persons to whom the Software is 73 | furnished to do so, subject to the following conditions: 74 | 75 | The above copyright notice and this permission notice shall be included in all 76 | copies or substantial portions of the Software. 77 | 78 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 79 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 80 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 81 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 82 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 83 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 84 | SOFTWARE. 85 | ``` -------------------------------------------------------------------------------- /bot.py: -------------------------------------------------------------------------------- 1 | import os, io, random, shortuuid 2 | import discord 3 | 4 | from discord.ext import commands 5 | 6 | from generator import Generator 7 | from model import Statistics 8 | 9 | from chess import * 10 | 11 | from threading import Timer 12 | 13 | from datetime import datetime 14 | from PIL import Image 15 | from dotenv import load_dotenv 16 | 17 | load_dotenv() 18 | 19 | TOKEN = os.getenv("TOKEN") 20 | 21 | intents = discord.Intents.all() 22 | 23 | bot = commands.Bot(command_prefix = "!", case_insensitive = True, intents = intents) 24 | 25 | class Invite: 26 | invites = list() 27 | 28 | def __init__(self, challenger: discord.Member, challenged: discord.Member, guild: discord.Guild): 29 | self.challenger, self.challenged = challenger, challenged 30 | self.guild = guild 31 | self.timestamp = datetime.now().strftime("%d-%m-%Y %H:%M") 32 | 33 | Timer(300.0, Invite.expire, [ self ]).start() 34 | 35 | @staticmethod 36 | def expire(invite): 37 | invites = Invite.invites 38 | 39 | if invite in invites: 40 | invites.remove(invite) 41 | 42 | class Game: 43 | games = list() 44 | 45 | def __init__(self, white: discord.Member, black: discord.Member, guild: discord.Guild): 46 | self.id = shortuuid.ShortUUID().random(length = 6) 47 | self.white, self.black = white, black 48 | self.guild = guild 49 | self.board = Board() 50 | 51 | async def send_error(ctx, title: str = "Error", description: str = "General internal error") -> None: 52 | title, description = f"**{title}:**", f"__{description}__" 53 | embed = discord.Embed(color = 0xff0000) 54 | embed.add_field(name = title, value = description, inline = True) 55 | await ctx.send(embed = embed) 56 | 57 | @bot.event 58 | async def on_ready() -> None: 59 | print("The BOT is currently online, connect to a Discord Server which contains it to start playing!") 60 | print("The name of the BOT is:", bot.user.name) 61 | print("The ID of the BOT is:", bot.user.id) 62 | print(datetime.now().strftime("%d-%m-%Y %H:%M")) 63 | 64 | @bot.event 65 | async def on_command_error(ctx, error): 66 | if isinstance(error, commands.CommandNotFound): 67 | return await send_error(ctx, description = "Unknown command, try !help to list every available command") 68 | 69 | if isinstance(error, commands.BadArgument): 70 | return await send_error(ctx, description = "The passed arguments are not valid for the invoked command") 71 | 72 | if isinstance(error, commands.MissingRequiredArgument): 73 | return await send_error(ctx, description = "You forgot to enter some mandatory parameters for the command execution") 74 | 75 | raise error 76 | 77 | class Main(commands.Cog): 78 | def __init__(self, bot): 79 | self.bot = bot 80 | 81 | @commands.command(name = "rnd", aliases = [ "random" ], help = "Draws a random number between `minimum` and `maximum`. If `minimum` and `maximum` are omitted, a number from `0` to `100` will be drawn") 82 | async def rnd(self, ctx, minimum: int = None, maximum: int = None): 83 | if minimum is None and maximum is None: minimum, maximum = 0, 100 84 | 85 | if (minimum is not None and maximum is None) or (minimum is None and maximum is not None): 86 | await send_error(ctx, description = "You must also indicate a maximum limit") 87 | 88 | if minimum < maximum: 89 | await ctx.send(f"{ctx.message.author.mention} draws a random number between `{minimum}` and `{maximum}` and gets: `{random.randint(minimum, maximum)}`") 90 | else: 91 | await send_error(ctx, description = "The minimum value must be less than the maximum one") 92 | 93 | @commands.command(name = "coin", aliases = [ "flip", "coinflip" ], help = "Flips a coin and returns the result") 94 | async def coin(self, ctx): 95 | author = ctx.message.author 96 | result = random.choice(["HEADS", "TAILS"]) 97 | await ctx.send(f"{author.mention} flips a coin and gets: `{result}`") 98 | 99 | @commands.command(name = "about", aliases = [ "info", "whois" ], help = "Shows different kind of informations about a specified and connected `user`. If `user` is omitted, the context user info will be shown.") 100 | async def about(self, ctx, user = None): 101 | author, guild = ctx.message.author, ctx.message.author.guild 102 | 103 | member = Main.get_member_by_name(guild, user or author.name) 104 | 105 | if member is None: 106 | return await send_error(ctx, description = "No user found in the current server") 107 | 108 | embed = discord.Embed(color = 0xff8040) 109 | 110 | value = ( 111 | f"Username: {member.name}\n" 112 | f"Discriminator: {member.discriminator}\n" 113 | f"Server nickname: {member.nick}\n" 114 | f"GUID: {member.id}\n" 115 | f"Avatar URL: {member.avatar_url}" 116 | ) 117 | 118 | embed.add_field(name = f"Who is {member.name}#{member.discriminator}:", value = value) 119 | 120 | await ctx.send(embed = embed) 121 | 122 | @staticmethod 123 | def get_member_by_name(guild: discord.Guild, name: str) -> discord.Member: 124 | if name is None: return None 125 | 126 | for member in guild.members: 127 | if member.name.lower() == name.lower(): 128 | return member 129 | 130 | if member.nick is not None: 131 | if member.nick.lower() == name.lower(): 132 | return member 133 | 134 | return None 135 | 136 | class Chess(commands.Cog): 137 | def __init__(self, bot): 138 | self.bot = bot 139 | 140 | @commands.group(name = "chess", help = "It contains all the main and unique commands of Chess-Bot to be able to play chess against your friends, or perhaps your enemies...") 141 | async def chess(self, ctx): 142 | if ctx.invoked_subcommand is None: 143 | raise commands.CommandNotFound 144 | 145 | @chess.command(name = "new", aliases = [ "create", "invite" ], help = "Sends an invite for a new match to a specified `user`") 146 | async def new(self, ctx, user = None): 147 | author, guild = ctx.message.author, ctx.message.author.guild 148 | 149 | user = Main.get_member_by_name(guild, user) 150 | 151 | if author == user: return await send_error(ctx, description = "You cannot challenge your-self") 152 | 153 | if user is None: return await send_error(ctx, description = "Please, specify a valid user to challenge") 154 | 155 | if self.get_game_from_user(user): return await send_error(ctx, description = "You cannot send invitations to a person who is already playing") 156 | 157 | if self.get_game_from_user(author): return await send_error(ctx, description = "You cannot send other invitations while you are in the middle of a match") 158 | 159 | for invite in Invite.invites: 160 | if invite.challenger == author and invite.challenged == user and invite.guild == guild: 161 | return await send_error(ctx, description = "You have already invited this user") 162 | 163 | Invite.invites.append(Invite(author, user, guild)) 164 | 165 | await ctx.send(f"Ehy {user.mention}, {author.name} wants to play a chess match against you! Use `!chess accept {author.name}` if you want to accept the invite") 166 | 167 | @chess.command(name = "accept", help = "Accepts an invite sent by a specified `user` and starts a new match") 168 | async def accept(self, ctx, user = None): 169 | author, guild = ctx.message.author, ctx.message.author.guild 170 | 171 | user = Main.get_member_by_name(guild, user) 172 | 173 | if user is None: return await send_error(ctx, description = "Please, specify a valid user to accept his invite") 174 | 175 | if self.get_game_from_user(user): return await send_error(ctx, description = "The selected player is already playing, wait until he is done") 176 | 177 | if self.get_game_from_user(author): return await send_error(ctx, description = "You cannot accept other invitations while you are in the middle of a match") 178 | 179 | invite = None 180 | 181 | for index, element in enumerate(Invite.invites): 182 | if element.challenged == author and element.challenger == user and element.guild == guild: 183 | invite = element; del Invite.invites[index]; break 184 | 185 | if invite is None: return await send_error(ctx, description = "No invite has been sent by the selected user") 186 | 187 | white, black = random.sample([author, user], 2) 188 | 189 | game = Game(white, black, guild); Game.games.append(game) 190 | 191 | file = Chess.get_binary_board(game.board) 192 | 193 | await ctx.send(( 194 | f"**Match ID: {game.id}**\n" 195 | f"Well, let's start this chess match with {white.mention} as `White Player` against {black.mention} as `Black Player`!" 196 | ), file = file) 197 | 198 | @chess.command(name = "invites", help = "Shows every out-coming and in-coming invites for the context user") 199 | async def invites(self, ctx): 200 | author, guild = ctx.message.author, ctx.message.author.guild 201 | 202 | embed = discord.Embed(title = f"Invitations for {author.name}", color = 0x00ff00) 203 | outcoming, incoming = str(), str() 204 | 205 | for invite in Invite.invites: 206 | if invite.challenger == author and invite.guild == guild: 207 | challenged = invite.challenged 208 | outcoming += f"You to {challenged.name} - {invite.timestamp}\n"; continue 209 | 210 | if invite.challenged == author and invite.guild == guild: 211 | challenger = invite.challenger 212 | incoming += f"{challenger.name} to you - {invite.timestamp}\n"; continue 213 | 214 | if not outcoming: outcoming = "No out-coming invitations for you" 215 | embed.add_field(name = ":arrow_right: Out-coming invites", value = outcoming, inline = False) 216 | 217 | if not incoming: incoming = "No in-coming invitations for you" 218 | embed.add_field(name = ":arrow_left: In-coming invites", value = incoming, inline = False) 219 | 220 | await ctx.send(embed = embed) 221 | 222 | @chess.command(name = "move", aliases = [ "execute" ], help = "Executes a move during a chess match") 223 | async def move(self, ctx, initial, final): 224 | author = ctx.message.author 225 | 226 | game = self.get_game_from_user(author) 227 | 228 | if game is None: return await send_error(ctx, description = "You are not playing any match in this server") 229 | 230 | color = None 231 | 232 | if game.white == author: color = WHITE 233 | if game.black == author: color = BLACK 234 | 235 | if color is not game.board.turn: return await send_error(ctx, description = "It is not your turn to make a move") 236 | 237 | message = f"{game.white.mention} VS {game.black.mention} - Actual turn: `{('BLACK', 'WHITE')[not game.board.turn]}`" 238 | 239 | try: initial = parse_square(initial) 240 | except ValueError: return await send_error(ctx, description = "The initial square is invalid. Check that its format is correct: + ") 241 | 242 | try: final = parse_square(final) 243 | except ValueError: return await send_error(ctx, description = "The final square is invalid. Check that its format is correct: + ") 244 | 245 | move = Move(initial, final) 246 | 247 | if move not in game.board.legal_moves: 248 | return await send_error(ctx, description = "Illegal move for the selected piece") 249 | 250 | game.board.push(move) 251 | 252 | if game.board.is_checkmate(): 253 | message = f"**Match Finished** - {game.white.mention} VS {game.black.mention} - `{author.name}` won the chess match, CONGRATULATIONS!" 254 | 255 | Game.games.remove(game) 256 | 257 | if color == WHITE: Chess.update_statitics(game.white, game.black) 258 | if color == BLACK: Chess.update_statitics(game.black, game.white) 259 | 260 | file = Chess.get_binary_board(game.board) 261 | 262 | await ctx.send(message, file = file) 263 | 264 | @chess.command(name = "show", aliases = [ "chessboard" ], help = "Shows the current match chessboard disposition") 265 | async def show(self, ctx): 266 | author = ctx.message.author 267 | 268 | game = self.get_game_from_user(author) 269 | 270 | if game is None: return await send_error(ctx, description = "You are not playing any match in this server") 271 | 272 | file = Chess.get_binary_board(game.board) 273 | 274 | await ctx.send(f"{game.white.mention} VS {game.black.mention} - Actual turn: `{('BLACK', 'WHITE')[game.board.turn]}`", file = file) 275 | 276 | @chess.command(name = "surrend", help = "Surrend and lost the current match") 277 | async def surrend(self, ctx): 278 | author = ctx.message.author 279 | 280 | game = self.get_game_from_user(author) 281 | 282 | if game is None: return await send_error(ctx, description = "You are not playing any match in this server") 283 | 284 | winner = None 285 | 286 | if game.white == author: winner = game.black 287 | if game.black == author: winner = game.white 288 | 289 | Game.games.remove(game) 290 | 291 | if game.white == author: Chess.update_statitics(game.black, game.white) 292 | if game.black == author: Chess.update_statitics(game.white, game.black) 293 | 294 | await ctx.send(f"{game.white.mention} VS {game.black.mention} - `{author.name}` surrended, `{winner.name}` won the match, CONGRATULATIONS!") 295 | 296 | @chess.command(name = "statistics", aliases = [ "stats" ], help = "Shows the statistics of a Discord user who played at least one match. If `user` is omitted, the context user stats will be shown") 297 | async def statistics(self, ctx, user = None): 298 | author, guild = ctx.message.author, ctx.message.author.guild 299 | 300 | member = Main.get_member_by_name(guild, user or author.name) 301 | 302 | if member is None: return await send_error(ctx, description = "No user found in the current server") 303 | 304 | embed = discord.Embed(color = 0x0000ff) 305 | 306 | try: 307 | statistics = Statistics.objects.get(player = member.id) 308 | 309 | value = ( 310 | f":vs: Number of matches played: {statistics.totals}\n\n" 311 | f":blue_circle: Number of matches won: {statistics.wins}\n" 312 | f":red_circle: Number of matches lost: {statistics.losts}\n\n" 313 | f":clock4: Last match date: {statistics.timestamp.strftime('%d-%m-%Y %H:%M')}" 314 | ) 315 | 316 | embed.add_field(name = f"Chess-Bot Statistics of {member.name}#{member.discriminator}", value = value) 317 | except: 318 | embed.add_field(name = "Information:", value = "The selected player has never played a game, his stats are therefore not available") 319 | 320 | await ctx.send(embed = embed) 321 | 322 | def get_game_from_user(self, user: discord.Member) -> Board: 323 | for game in Game.games: 324 | if (game.white == user or game.black == user) and game.guild == user.guild: 325 | return game 326 | 327 | return None 328 | 329 | @staticmethod 330 | def get_binary_board(board) -> discord.File: 331 | size = (500, 500) 332 | 333 | with io.BytesIO() as binary: 334 | board = Generator.generate(board).resize(size, Image.ANTIALIAS) 335 | board.save(binary, "PNG"); binary.seek(0) 336 | return discord.File(fp = binary, filename = "board.png") 337 | 338 | @staticmethod 339 | def update_statitics(winner: discord.Member, loser: discord.Member): 340 | winner, loser = winner.id, loser.id 341 | 342 | try: winner = Statistics.objects.get(player = winner) 343 | except: winner = Statistics(player = winner) 344 | 345 | try: loser = Statistics.objects.get(player = loser) 346 | except: loser = Statistics(player = loser) 347 | 348 | winner.totals += 1; loser.totals += 1 349 | winner.wins += 1; loser.losts += 1 350 | 351 | now = datetime.now() 352 | 353 | winner.timestamp = now; loser.timestamp = now 354 | 355 | winner.save(); loser.save() 356 | 357 | bot.add_cog(Main(bot)) 358 | 359 | bot.add_cog(Chess(bot)) 360 | 361 | bot.run(TOKEN) --------------------------------------------------------------------------------