diff --git a/.gitignore b/.gitignore index 2895fff3..afb0533d 100644 --- a/.gitignore +++ b/.gitignore @@ -102,3 +102,4 @@ ENV/ # PyCharm .idea/ +.vscode/settings.json diff --git a/bot/cogs/snakes.py b/bot/cogs/snakes.py index c9ed8042..6eaf6b69 100644 --- a/bot/cogs/snakes.py +++ b/bot/cogs/snakes.py @@ -1,9 +1,16 @@ # coding=utf-8 import logging +import json +from aiohttp import ClientSession +from random import choice +from time import time from typing import Any, Dict +from discord import Embed, Color from discord.ext.commands import AutoShardedBot, Context, command +from bot.snakegame import SnakeGame + log = logging.getLogger(__name__) @@ -14,7 +21,15 @@ class Snakes: def __init__(self, bot: AutoShardedBot): self.bot = bot + self.game = SnakeGame((5, 5)) + self.debug = True + # changed this to (User.id: int) in order to make it easier down the line to call. >(PC) + self.mods = [255254195505070081, 98694745760481280] + self.last_movement = time() + self.movement_command = {"left": 0, "right": 0, "up": 0, "down": 0} + self.wait_time = 2 + # docstring for get_snek needs to be cleaned up >(PC) async def get_snek(self, name: str = None) -> Dict[str, Any]: """ Go online and fetch information about a snake @@ -29,6 +44,16 @@ async def get_snek(self, name: str = None) -> Dict[str, Any]: :return: A dict containing information on a snake """ + url = f'https://en.wikipedia.org/w/api.php?action=query&titles={name}' \ + f'&prop=extracts&exlimit=1&explaintext&format=json&formatversion=2' + + # account for snakes without a page somewhere. >(PC) + async with ClientSession() as session: + async with session.get(url) as response: + resp = json.loads(str(await response.read(), 'utf-8')) + return resp + + # docstring for get needs to be cleaned up. >(PC) @command() async def get(self, ctx: Context, name: str = None): """ @@ -40,8 +65,84 @@ async def get(self, ctx: Context, name: str = None): :param ctx: Context object passed from discord.py :param name: Optional, the name of the snake to get information for - omit for a random snake """ + # Everything with snek_list should be cached >(PC) + # SELF.BOT.SNEK_LIST OMG OMG OMG >(PC) + # Since, on restart, the bot will forget this, it will be re-cached every time. Problem Solved in theory. >(PC) + possible_names = 'https://en.wikipedia.org/w/api.php?action=query&titles=List_of_snakes_by_common_name' \ + '&prop=extracts&exlimit=1&explaintext&format=json&formatversion=2' + + async with ClientSession() as session: + async with session.get(possible_names) as all_sneks: + resp = str(await all_sneks.read(), 'utf-8') + + # can we find a better way to do this? Doesn't seem too reliable, even though MW won't change their api. >(PC) + snek_list = resp[409:].lower().split('\\n') + + # if name is None, choose a random snake. Need to clean up snek_list. >(PC) + if name is None: + name = choice(snek_list) + + # stops the command if the snek is not on the list >(PC) + elif name.lower() not in snek_list: + await ctx.send('This is not a valid snake. Please request one that exists.\n' + 'You can find a list of existing snakes here: ') + return + + # accounting for the spaces in the names of some snakes. Allows for parsing of spaced names. >(PC) + if name.split(' '): + name = '%20'.join(name.split(' ')) + + # leaving off here for the evening. Building the embed is easy. Pulling the information is hard. /s >(PC) + # snek_dict = await self.get_snek(name) # Any additional commands can be placed here. Be creative, but keep it to a reasonable amount! + @command() + async def play(self, ctx: Context, order): + """ + DiscordPlaysSnek + + Move the snek around the field and collect food. + + Valid use: `bot.play {direction}` + + With 'left', 'right', 'up' and 'down' as valid directions. + """ + + # Maybe one game at a given time, and maybe editing the original message instead of constantly posting + # new ones? Maybe we could also ask nicely for DMs to be allowed for this if they aren't. >(PC) + if order in self.movement_command.keys(): + self.movement_command[order] += 1 + + # check if it's time to move the snek + if time() - self.last_movement > self.wait_time: + direction = max(self.movement_command, + key=self.movement_command.get) + percentage = 100*self.movement_command[direction]/sum(self.movement_command.values()) + move_status = self.game.move(direction) + + # end game + if move_status == "lost": + await ctx.send("We made the snek cry! :snake: :cry:") + + # prepare snek message + snekembed = Embed(color=Color.red()) + snekembed.add_field(name="Score", value=self.game.score) + snekembed.add_field(name="Winner movement", + value="{dir}: {per:.2f}%".format(dir=direction, per=percentage)) + + snek_head = next(emoji for emoji in ctx.guild.emojis if emoji.name == 'python') + + game_string = str(self.game).replace(":python:", str(snek_head)) + snekembed.add_field(name="Board", value=game_string, inline=False) + if self.debug: + snekembed.add_field( + name="Debug", value="Debug - move_status: " + move_status, inline=False) + + # prepare next movement + self.last_movement = time() + self.movement_command = {"left": 0, + "right": 0, "up": 0, "down": 0} + await ctx.send(embed=snekembed) def setup(bot): diff --git a/bot/snakegame.py b/bot/snakegame.py new file mode 100644 index 00000000..b3520a0b --- /dev/null +++ b/bot/snakegame.py @@ -0,0 +1,149 @@ +from random import randint + + +class SnakeGame: + """ + Simple Snake game + """ + + def __init__(self, board_dimensions): + self.board_dimensions = board_dimensions + self.restart() + + def restart(self): + """ + Restores game to default state + """ + + self.snake = Snake(positions=[[2, 2], [2, 1]]) + self.putFood() + self.score = 0 + + def __str__(self): + """ + Draw the board + """ + # create empty board + # board_string = "+" + "-" * (self.board_dimensions[1]) + "+" + "\n" + board_string = "" + for i in range(self.board_dimensions[0]): + # row_string = "|" + row_string = "" + # draw snake + for j in range(self.board_dimensions[1]): + if [i, j] == self.snake.positions[0]: + row_string += ":python:" # head + elif [i, j] in self.snake.positions: + row_string += "🐍" + elif [i, j] == self.food: + row_string += "πŸ•" + else: + row_string += "◻️" + # row_string += "|\n" + board_string += row_string + "\n" + # board_string += "+" + "-" * (self.board_dimensions[1]) + "+\n" + + return board_string + + def move(self, direction): + """ + Executes one movement. + Returns information about the movement: + "ok", "forbidden", "lost", "food". + """ + + direction_dict = { + "right": (0, 1), + "left": (0, -1), + "up": (-1, 0), + "down": (1, 0) + } + move_status = self.snake.move(direction_dict[direction]) + + if self.isLost(): + move_status = "lost" + self.restart() + + if self.isEating(): + self.snake.grow() + move_status = "grow" + self.putFood() + self.score += 1 + + return move_status + + def isLost(self): + head = self.snake.head + + if (head[0] == -1 or + head[1] == -1 or + head[0] == self.board_dimensions[0] or + head[1] == self.board_dimensions[1] or + head in self.snake.positions[1:]): + return True + + return False + + def putFood(self): + valid = False + while not valid: + i = randint(0, self.board_dimensions[0] - 1) + j = randint(0, self.board_dimensions[1] - 1) + + if [i, j] not in self.snake.positions: + valid = True + + self.food = [i, j] + + def isEating(self): + if self.snake.head == self.food: + return True + return False + + +class Snake: + """ + Actual snake in the game. + """ + + def __init__(self, positions): + self.positions = positions + self.head = positions[0] + + def move(self, velocity): + """ + Executes one movement. + + Returns information about the movement: + "ok", "forbidden" + """ + if not self.isPossible(velocity): + print("Movement not allowed") + return "forbidden" + + # delete tail but store it, as we might want the snake to grow + self.deletedTail = self.positions[-1] + self.positions = self.positions[:-1] + + # move head + self.head = [self.head[0] + velocity[0], + self.head[1] + velocity[1]] + self.positions.insert(0, self.head) + + return "ok" + + def isPossible(self, velocity): + """ + Check Snake is trying to do an 180ΒΊ turn. + """ + newHead = [self.head[0] + velocity[0], + self.head[1] + velocity[1]] + if newHead == self.positions[1]: + return False + return True + + def grow(self): + """ + Makes the snake grow one square. + """ + self.positions.append(self.deletedTail)