aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--bot/pagination.py85
-rw-r--r--bot/resources/evergreen/game_recs/chrono_trigger.json7
-rw-r--r--bot/resources/evergreen/game_recs/digimon_world.json7
-rw-r--r--bot/resources/evergreen/game_recs/doom_2.json7
-rw-r--r--bot/resources/evergreen/game_recs/skyrim.json7
-rw-r--r--bot/seasons/evergreen/minesweeper.py29
-rw-r--r--bot/seasons/evergreen/recommend_game.py51
-rw-r--r--bot/seasons/evergreen/snakes/snakes_cog.py24
-rw-r--r--bot/seasons/evergreen/snakes/utils.py16
-rw-r--r--bot/seasons/halloween/monstersurvey.py25
-rw-r--r--bot/seasons/valentines/be_my_valentine.py5
-rw-r--r--bot/utils/__init__.py16
12 files changed, 140 insertions, 139 deletions
diff --git a/bot/pagination.py b/bot/pagination.py
index e6cea41f..c12b6233 100644
--- a/bot/pagination.py
+++ b/bot/pagination.py
@@ -22,26 +22,16 @@ class EmptyPaginatorEmbed(Exception):
class LinePaginator(Paginator):
- """
- A class that aids in paginating code blocks for Discord messages.
-
- Attributes
- -----------
- prefix: :class:`str`
- The prefix inserted to every page. e.g. three backticks.
- suffix: :class:`str`
- The suffix appended at the end of every page. e.g. three backticks.
- max_size: :class:`int`
- The maximum amount of codepoints allowed in a page.
- max_lines: :class:`int`
- The maximum amount of lines allowed in a page.
- """
+ """A class that aids in paginating code blocks for Discord messages."""
def __init__(self, prefix='```', suffix='```', max_size=2000, max_lines=None):
"""
Overrides the Paginator.__init__ from inside discord.ext.commands.
- Allows for configuration of the maximum number of lines per page.
+ `prefix` and `suffix` will be prepended and appended respectively to every page.
+
+ `max_size` and `max_lines` denote the maximum amount of codepoints and lines
+ allowed per page.
"""
self.prefix = prefix
self.suffix = suffix
@@ -56,22 +46,12 @@ class LinePaginator(Paginator):
"""
Adds a line to the current page.
- If the line exceeds the `max_size` then an exception is raised.
+ If the line exceeds the `max_size` then a RuntimeError is raised.
Overrides the Paginator.add_line from inside discord.ext.commands in order to allow
configuration of the maximum number of lines per page.
- Parameters
- -----------
- line: str
- The line to add.
- empty: bool
- Indicates if another empty line should be added.
-
- Raises
- ------
- RuntimeError
- The line was too big for the current `max_size`.
+ If `empty` is True, an empty line will be placed after the a given `line`.
"""
if len(line) > self.max_size - len(self.prefix) - 2:
raise RuntimeError('Line exceeds maximum page size %s' % (self.max_size - len(self.prefix) - 2))
@@ -105,7 +85,11 @@ class LinePaginator(Paginator):
When used, this will send a message using `ctx.send()` and apply a set of reactions to it.
These reactions may be used to change page, or to remove pagination from the message.
- Pagination will also be removed automatically if no reaction is added for five minutes (300 seconds).
+
+ Pagination will also be removed automatically if no reaction is added for `timeout` seconds,
+ defaulting to five minutes (300 seconds).
+
+ If `empty` is True, an empty line will be placed between each given line.
>>> embed = Embed()
>>> embed.set_author(name="Some Operation", url=url, icon_url=icon)
@@ -113,20 +97,6 @@ class LinePaginator(Paginator):
... (line for line in lines),
... ctx, embed
... )
-
- :param lines: The lines to be paginated
- :param ctx: Current context object
- :param embed: A pre-configured embed to be used as a template for each page
- :param prefix: Text to place before each page
- :param suffix: Text to place after each page
- :param max_lines: The maximum number of lines on each page
- :param max_size: The maximum number of characters on each page
- :param empty: Whether to place an empty line between each given line
- :param restrict_to_user: A user to lock pagination operations to for this message, if supplied
- :param exception_on_empty_embed: Should there be an exception if the embed is empty?
- :param url: the url to use for the embed headline
- :param timeout: The amount of time in seconds to disable pagination of no reaction is added
- :param footer_text: Text to prefix the page number in the footer with
"""
def event_check(reaction_: Reaction, user_: Member):
"""Make sure that this reaction is what we want to operate on."""
@@ -314,8 +284,7 @@ class ImagePaginator(Paginator):
"""
Adds a line to each page, usually just 1 line in this context.
- :param line: str to be page content / title
- :param empty: if there should be new lines between entries
+ If `empty` is True, an empty line will be placed after a given `line`.
"""
if line:
self._count = len(line)
@@ -325,11 +294,7 @@ class ImagePaginator(Paginator):
self.close_page()
def add_image(self, image: str = None) -> None:
- """
- Adds an image to a page.
-
- :param image: image url to be appended
- """
+ """Adds an image to a page given the url."""
self.images.append(image)
@classmethod
@@ -339,33 +304,21 @@ class ImagePaginator(Paginator):
"""
Use a paginator and set of reactions to provide pagination over a set of title/image pairs.
- The reactions are used to switch page, or to finish with pagination.
+ `pages` is a list of tuples of page title/image url pairs.
+ `prefix` and `suffix` will be prepended and appended respectively to the message.
When used, this will send a message using `ctx.send()` and apply a set of reactions to it.
These reactions may be used to change page, or to remove pagination from the message.
- Note: Pagination will be removed automatically if no reaction is added for five minutes (300 seconds).
+ Note: Pagination will be removed automatically if no reaction is added for `timeout` seconds,
+ defaulting to five minutes (300 seconds).
>>> embed = Embed()
>>> embed.set_author(name="Some Operation", url=url, icon_url=icon)
>>> await ImagePaginator.paginate(pages, ctx, embed)
-
- Parameters
- -----------
- :param pages: An iterable of tuples with title for page, and img url
- :param ctx: ctx for message
- :param embed: base embed to modify
- :param prefix: prefix of message
- :param suffix: suffix of message
- :param timeout: timeout for when reactions get auto-removed
"""
def check_event(reaction_: Reaction, member: Member) -> bool:
- """
- Checks each reaction added, if it matches our conditions pass the wait_for.
-
- :param reaction_: reaction added
- :param member: reaction added by member
- """
+ """Checks each reaction added, if it matches our conditions pass the wait_for."""
return all((
# Reaction is on the same message sent
reaction_.message.id == message.id,
diff --git a/bot/resources/evergreen/game_recs/chrono_trigger.json b/bot/resources/evergreen/game_recs/chrono_trigger.json
new file mode 100644
index 00000000..219c1e78
--- /dev/null
+++ b/bot/resources/evergreen/game_recs/chrono_trigger.json
@@ -0,0 +1,7 @@
+{
+ "title": "Chrono Trigger",
+ "description": "One of the best games of all time. A brilliant story involving time-travel with loveable characters. It has a brilliant score by Yasonuri Mitsuda and artwork by Akira Toriyama. With over 20 endings and New Game+, there is a huge amount of replay value here.",
+ "link": "https://rawg.io/games/chrono-trigger-1995",
+ "image": "https://vignette.wikia.nocookie.net/chrono/images/2/24/Chrono_Trigger_cover.jpg",
+ "author": "352635617709916161"
+} \ No newline at end of file
diff --git a/bot/resources/evergreen/game_recs/digimon_world.json b/bot/resources/evergreen/game_recs/digimon_world.json
new file mode 100644
index 00000000..a2820f8e
--- /dev/null
+++ b/bot/resources/evergreen/game_recs/digimon_world.json
@@ -0,0 +1,7 @@
+{
+ "title": "Digimon World",
+ "description": "A great mix of town-building and pet-raising set in the Digimon universe. With plenty of Digimon to raise and recruit to the village, this charming game will keep you occupied for a long time.",
+ "image": "https://www.mobygames.com/images/covers/l/437308-digimon-world-playstation-front-cover.jpg",
+ "link": "https://rawg.io/games/digimon-world",
+ "author": "352635617709916161"
+} \ No newline at end of file
diff --git a/bot/resources/evergreen/game_recs/doom_2.json b/bot/resources/evergreen/game_recs/doom_2.json
new file mode 100644
index 00000000..e228e2b1
--- /dev/null
+++ b/bot/resources/evergreen/game_recs/doom_2.json
@@ -0,0 +1,7 @@
+{
+ "title": "Doom II",
+ "description": "Doom 2 was one of the first FPS games that I truly enjoyed. It offered awesome weapons, terrifying demons to kill, and a great atmosphere to do it in.",
+ "image": "https://upload.wikimedia.org/wikipedia/en/thumb/2/29/Doom_II_-_Hell_on_Earth_Coverart.png/220px-Doom_II_-_Hell_on_Earth_Coverart.png",
+ "link": "https://rawg.io/games/doom-ii",
+ "author": "352635617709916161"
+} \ No newline at end of file
diff --git a/bot/resources/evergreen/game_recs/skyrim.json b/bot/resources/evergreen/game_recs/skyrim.json
new file mode 100644
index 00000000..09f93563
--- /dev/null
+++ b/bot/resources/evergreen/game_recs/skyrim.json
@@ -0,0 +1,7 @@
+{
+ "title": "Elder Scrolls V: Skyrim",
+ "description": "The latest mainline Elder Scrolls game offered a fantastic role-playing experience with untethered freedom and a great story. Offering vast mod support, the game has endless customization and replay value.",
+ "image": "https://upload.wikimedia.org/wikipedia/en/1/15/The_Elder_Scrolls_V_Skyrim_cover.png",
+ "link": "https://rawg.io/games/the-elder-scrolls-v-skyrim",
+ "author": "352635617709916161"
+} \ No newline at end of file
diff --git a/bot/seasons/evergreen/minesweeper.py b/bot/seasons/evergreen/minesweeper.py
index 9f6aff95..cb859ea9 100644
--- a/bot/seasons/evergreen/minesweeper.py
+++ b/bot/seasons/evergreen/minesweeper.py
@@ -1,7 +1,7 @@
import logging
import typing
from dataclasses import dataclass
-from random import random
+from random import randint, random
import discord
from discord.ext import commands
@@ -22,7 +22,8 @@ MESSAGE_MAPPING = {
10: ":keycap_ten:",
"bomb": ":bomb:",
"hidden": ":grey_question:",
- "flag": ":pyflag:"
+ "flag": ":flag_black:",
+ "x": ":x:"
}
log = logging.getLogger(__name__)
@@ -36,7 +37,13 @@ class CoordinateConverter(commands.Converter):
if not 2 <= len(coordinate) <= 3:
raise commands.BadArgument('Invalid co-ordinate provided')
- digit, letter = sorted(coordinate.lower())
+ coordinate = coordinate.lower()
+ if coordinate[0].isalpha():
+ digit = coordinate[1:]
+ letter = coordinate[0]
+ else:
+ digit = coordinate[:-1]
+ letter = coordinate[-1]
if not digit.isdigit():
raise commands.BadArgument
@@ -93,6 +100,10 @@ class Minesweeper(commands.Cog):
for _ in range(10)
] for _ in range(10)
]
+
+ # make sure there is always a free cell
+ board[randint(0, 9)][randint(0, 9)] = "number"
+
for y, row in enumerate(board):
for x, cell in enumerate(row):
if cell == "number":
@@ -172,10 +183,18 @@ class Minesweeper(commands.Cog):
await self.update_boards(ctx)
+ @staticmethod
+ def reveal_bombs(revealed: GameBoard, board: GameBoard) -> None:
+ """Reveals all the bombs"""
+ for y, row in enumerate(board):
+ for x, cell in enumerate(row):
+ if cell == "bomb":
+ revealed[y][x] = cell
+
async def lost(self, ctx: commands.Context) -> None:
"""The player lost the game"""
game = self.games[ctx.author.id]
- game.revealed = game.board
+ self.reveal_bombs(game.revealed, game.board)
await ctx.author.send(":fire: You lost! :fire:")
if game.activated_on_server:
await game.chat_msg.channel.send(f":fire: {ctx.author.mention} just lost Minesweeper! :fire:")
@@ -183,7 +202,6 @@ class Minesweeper(commands.Cog):
async def won(self, ctx: commands.Context) -> None:
"""The player won the game"""
game = self.games[ctx.author.id]
- game.revealed = game.board
await ctx.author.send(":tada: You won! :tada:")
if game.activated_on_server:
await game.chat_msg.channel.send(f":tada: {ctx.author.mention} just won Minesweeper! :tada:")
@@ -225,6 +243,7 @@ class Minesweeper(commands.Cog):
revealed[y][x] = board[y][x]
if board[y][x] == "bomb":
await self.lost(ctx)
+ revealed[y][x] = "x" # mark bomb that made you lose with a x
return True
elif board[y][x] == 0:
self.reveal_zeros(revealed, board, x, y)
diff --git a/bot/seasons/evergreen/recommend_game.py b/bot/seasons/evergreen/recommend_game.py
new file mode 100644
index 00000000..835a4e53
--- /dev/null
+++ b/bot/seasons/evergreen/recommend_game.py
@@ -0,0 +1,51 @@
+import json
+import logging
+from pathlib import Path
+from random import shuffle
+
+import discord
+from discord.ext import commands
+
+log = logging.getLogger(__name__)
+game_recs = []
+
+# Populate the list `game_recs` with resource files
+for rec_path in Path("bot/resources/evergreen/game_recs").glob("*.json"):
+ with rec_path.open(encoding='utf-8') as file:
+ data = json.load(file)
+ game_recs.append(data)
+shuffle(game_recs)
+
+
+class RecommendGame(commands.Cog):
+ """Commands related to recommending games."""
+
+ def __init__(self, bot: commands.Bot) -> None:
+ self.bot = bot
+ self.index = 0
+
+ @commands.command(name="recommendgame", aliases=['gamerec'])
+ async def recommend_game(self, ctx: commands.Context) -> None:
+ """Sends an Embed of a random game recommendation."""
+ if self.index >= len(game_recs):
+ self.index = 0
+ shuffle(game_recs)
+ game = game_recs[self.index]
+ self.index += 1
+
+ author = self.bot.get_user(int(game['author']))
+
+ # Creating and formatting Embed
+ embed = discord.Embed(color=discord.Colour.blue())
+ if author is not None:
+ embed.set_author(name=author.name, icon_url=author.avatar_url)
+ embed.set_image(url=game['image'])
+ embed.add_field(name='Recommendation: ' + game['title'] + '\n' + game['link'], value=game['description'])
+
+ await ctx.send(embed=embed)
+
+
+def setup(bot: commands.Bot) -> None:
+ """Loads the RecommendGame cog."""
+ bot.add_cog(RecommendGame(bot))
+ log.info("RecommendGame cog loaded")
diff --git a/bot/seasons/evergreen/snakes/snakes_cog.py b/bot/seasons/evergreen/snakes/snakes_cog.py
index 1d138aff..38878706 100644
--- a/bot/seasons/evergreen/snakes/snakes_cog.py
+++ b/bot/seasons/evergreen/snakes/snakes_cog.py
@@ -303,9 +303,6 @@ class Snakes(Cog):
Builds a dict that the .get() method can use.
Created by Ava and eivl.
-
- :param name: The name of the snake to get information for - omit for a random snake
- :return: A dict containing information on a snake
"""
snake_info = {}
@@ -403,11 +400,7 @@ class Snakes(Cog):
return snake_info
async def _get_snake_name(self) -> Dict[str, str]:
- """
- Gets a random snake name.
-
- :return: A random snake name, as a string.
- """
+ """Gets a random snake name."""
return random.choice(self.snake_names)
async def _validate_answer(self, ctx: Context, message: Message, answer: str, options: list):
@@ -631,14 +624,10 @@ class Snakes(Cog):
@snakes_group.command(name='get')
@bot_has_permissions(manage_messages=True)
@locked()
- async def get_command(self, ctx: Context, *, name: Snake = None):
+ async def get_command(self, ctx: Context, *, name: Snake = None) -> None:
"""
Fetches information about a snake from Wikipedia.
- :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
-
Created by Ava and eivl.
"""
with ctx.typing():
@@ -1034,10 +1023,8 @@ class Snakes(Cog):
"""
How would I talk if I were a snake?
- :param ctx: context
- :param message: If this is passed, it will snakify the message.
- If not, it will snakify a random message from
- the users history.
+ If `message` is passed, the bot will snakify the message.
+ Otherwise, a random message from the user's history is snakified.
Written by Momo and kel.
Modified by lemon.
@@ -1077,8 +1064,7 @@ class Snakes(Cog):
"""
Gets a YouTube video about snakes.
- :param ctx: Context object passed from discord.py
- :param search: Optional, a name of a snake. Used to search for videos with that name
+ If `search` is given, a snake with that name will be searched on Youtube.
Written by Andrew and Prithaj.
"""
diff --git a/bot/seasons/evergreen/snakes/utils.py b/bot/seasons/evergreen/snakes/utils.py
index 5d3b0dee..e8d2ee44 100644
--- a/bot/seasons/evergreen/snakes/utils.py
+++ b/bot/seasons/evergreen/snakes/utils.py
@@ -285,20 +285,8 @@ def create_snek_frame(
"""
Creates a single random snek frame using Perlin noise.
- :param perlin_factory: the perlin noise factory used. Required.
- :param perlin_lookup_vertical_shift: the Perlin noise shift in the Y-dimension for this frame
- :param image_dimensions: the size of the output image.
- :param image_margins: the margins to respect inside of the image.
- :param snake_length: the length of the snake, in segments.
- :param snake_color: the color of the snake.
- :param bg_color: the background color.
- :param segment_length_range: the range of the segment length. Values will be generated inside
- this range, including the bounds.
- :param snake_width: the width of the snek, in pixels.
- :param text: the text to display with the snek. Set to None for no text.
- :param text_position: the position of the text.
- :param text_color: the color of the text.
- :return: a PIL image, representing a single frame.
+ `perlin_lookup_vertical_shift` represents the Perlin noise shift in the Y-dimension for this frame.
+ If `text` is given, display the given text with the snek.
"""
start_x = random.randint(image_margins[X], image_dimensions[X] - image_margins[X])
start_y = random.randint(image_margins[Y], image_dimensions[Y] - image_margins[Y])
diff --git a/bot/seasons/halloween/monstersurvey.py b/bot/seasons/halloween/monstersurvey.py
index 4e967cca..173ce8eb 100644
--- a/bot/seasons/halloween/monstersurvey.py
+++ b/bot/seasons/halloween/monstersurvey.py
@@ -36,15 +36,11 @@ class MonsterSurvey(Cog):
with open(self.registry_location, 'w') as jason:
json.dump(self.voter_registry, jason, indent=2)
- def cast_vote(self, id: int, monster: str):
+ def cast_vote(self, id: int, monster: str) -> None:
"""
Cast a user's vote for the specified monster.
If the user has already voted, their existing vote is removed.
-
- :param id: The id of the person voting
- :param monster: the string key of the json that represents a monster
- :return: None
"""
vr = self.voter_registry
for m in vr.keys():
@@ -147,14 +143,8 @@ class MonsterSurvey(Cog):
@monster_group.command(
name='show'
)
- async def monster_show(self, ctx: Context, name=None):
- """
- Shows the named monster. If one is not named, it sends the default voting embed instead.
-
- :param ctx:
- :param name:
- :return:
- """
+ async def monster_show(self, ctx: Context, name=None) -> None:
+ """Shows the named monster. If one is not named, it sends the default voting embed instead."""
if name is None:
await ctx.invoke(self.monster_leaderboard)
return
@@ -184,13 +174,8 @@ class MonsterSurvey(Cog):
name='leaderboard',
aliases=('lb',)
)
- async def monster_leaderboard(self, ctx: Context):
- """
- Shows the current standings.
-
- :param ctx:
- :return:
- """
+ async def monster_leaderboard(self, ctx: Context) -> None:
+ """Shows the current standings."""
async with ctx.typing():
vr = self.voter_registry
top = sorted(vr, key=lambda k: len(vr[k]['votes']), reverse=True)
diff --git a/bot/seasons/valentines/be_my_valentine.py b/bot/seasons/valentines/be_my_valentine.py
index ac140896..c4acf17a 100644
--- a/bot/seasons/valentines/be_my_valentine.py
+++ b/bot/seasons/valentines/be_my_valentine.py
@@ -184,14 +184,11 @@ class BeMyValentine(commands.Cog):
return valentine, title
@staticmethod
- def random_user(author, members):
+ def random_user(author: discord.Member, members: discord.Member):
"""
Picks a random member from the list provided in `members`.
The invoking author is ignored.
-
- :param author: member who invoked the command
- :param members: list of discord.Member objects
"""
if author in members:
members.remove(author)
diff --git a/bot/utils/__init__.py b/bot/utils/__init__.py
index ad019357..3249a9cf 100644
--- a/bot/utils/__init__.py
+++ b/bot/utils/__init__.py
@@ -11,21 +11,15 @@ from bot.pagination import LinePaginator
async def disambiguate(
ctx: Context, entries: List[str], *, timeout: float = 30,
- per_page: int = 20, empty: bool = False, embed: discord.Embed = None
-):
+ entries_per_page: int = 20, empty: bool = False, embed: discord.Embed = None
+) -> str:
"""
Has the user choose between multiple entries in case one could not be chosen automatically.
+ Disambiguation will be canceled after `timeout` seconds.
+
This will raise a BadArgument if entries is empty, if the disambiguation event times out,
or if the user makes an invalid choice.
-
- :param ctx: Context object from discord.py
- :param entries: List of items for user to choose from
- :param timeout: Number of seconds to wait before canceling disambiguation
- :param per_page: Entries per embed page
- :param empty: Whether the paginator should have an extra line between items
- :param embed: The embed that the paginator will use.
- :return: Users choice for correct entry.
"""
if len(entries) == 0:
raise BadArgument('No matches found.')
@@ -45,7 +39,7 @@ async def disambiguate(
embed = discord.Embed()
coro1 = ctx.bot.wait_for('message', check=check, timeout=timeout)
- coro2 = LinePaginator.paginate(choices, ctx, embed=embed, max_lines=per_page,
+ coro2 = LinePaginator.paginate(choices, ctx, embed=embed, max_lines=entries_per_page,
empty=empty, max_size=6000, timeout=9000)
# wait_for timeout will go to except instead of the wait_for thing as I expected