11 Commits

Author SHA1 Message Date
ed1ea1f7af 2025-10-28 17:34:32 +01:00
f79a27aaca Merge branch 'complete-rework' of git.ingemanngade.net:NikolajDanger/Gwendolyn into complete-rework 2025-10-28 15:45:36 +01:00
18d7f318f6 2025-10-28 15:42:26 +01:00
Nikolaj
8c962a0b1a 2025-10-28 11:08:19 +01:00
020c686c81 2025-10-27 15:11:22 +01:00
7be6103a91 2024-11-06 16:54:56 +01:00
e71ba34371 2024-10-31 22:38:59 +01:00
Nikolaj
2f4e606fbf 2024-10-30 15:31:46 +01:00
e955ef4e28 2024-10-29 15:20:28 +01:00
Nikolaj
eb2960aa10 reworking a bunch of stuff 2.0 2024-10-28 14:38:41 +01:00
Nikolaj
bc59bf9b05 reworking a bunch of stuff 2024-10-28 13:05:06 +01:00
185 changed files with 10318 additions and 8629 deletions

4
.gitignore vendored
View File

@@ -150,9 +150,7 @@ static
.vscode/ .vscode/
token.txt .env
credentials.txt
options.txt
gwendolyn/resources/star_wars/destinyPoints.txt gwendolyn/resources/star_wars/destinyPoints.txt
gwendolyn/resources/plex/ gwendolyn/resources/plex/
gwendolyn/resources/games/hilo/ gwendolyn/resources/games/hilo/

View File

@@ -10,7 +10,6 @@ Gwendolyn is a discord bot that I made. It does a bunch of stuff.
* Roll Star Wars RPG dice! * Roll Star Wars RPG dice!
* Keep track of Star Wars RPG character sheets! * Keep track of Star Wars RPG character sheets!
* Query Wolfram Alpha * Query Wolfram Alpha
* Invest fake money in the stock market
* Play trivia, connect 4, blackjack, hangman and hex * Play trivia, connect 4, blackjack, hangman and hex
And much more!!! (not really. That's pretty much all it can do. See help files in resources directory for list of commands) And much more!!! (not really. That's pretty much all it can do. See help files in resources directory for list of commands)

View File

@@ -1,6 +1,5 @@
"""The main module for Gwendolyn.""" """The main module for Gwendolyn."""
# pylint: disable=invalid-name
__all__ = ["funcs", "utils", "Gwendolyn"] __all__ = ["Gwendolyn"]
from .gwendolyn_client import Gwendolyn from .gwendolyn_client import Gwendolyn

View File

@@ -1,40 +0,0 @@
"""Contains the EventCog, which runs code for specific bot events."""
from discord.ext import commands # Has the cog class
class EventCog(commands.Cog):
"""Handles bot events."""
def __init__(self, bot):
"""Initialize the bot."""
self.bot = bot
self.bot.on_error = self.on_error
@commands.Cog.listener()
async def on_ready(self):
"""Log and set bot status when bot logs in."""
await self.bot.event_handler.on_ready()
@commands.Cog.listener()
async def on_slash_command(self, ctx):
"""Log when a slash command is run."""
await self.bot.event_handler.on_slash_command(ctx)
@commands.Cog.listener()
async def on_slash_command_error(self, ctx, error):
"""Log when a slash error occurs."""
await self.bot.error_handler.on_slash_command_error(ctx, error)
async def on_error(self, method, *args, **kwargs): # pylint: disable=unused-argument
"""Log when an error occurs."""
await self.bot.error_handler.on_error(method)
@commands.Cog.listener()
async def on_component(self, ctx):
"""Handle when someone reacts to a message."""
await self.bot.event_handler.on_component(ctx)
def setup(bot):
"""Add the eventcog to the bot."""
bot.add_cog(EventCog(bot))

View File

@@ -1,165 +0,0 @@
"""Contains all the cogs that deal with game commands."""
from discord.ext import commands # Has the cog class
from discord_slash import cog_ext # Used for slash commands
from gwendolyn.utils import get_params # pylint: disable=import-error
params = get_params()
class GamesCog(commands.Cog):
"""Contains miscellaneous game commands."""
def __init__(self, bot):
"""Initialize the cog."""
self.bot = bot
@cog_ext.cog_slash(**params["balance"])
async def balance(self, ctx):
"""Check user balance."""
await self.bot.money.sendBalance(ctx)
@cog_ext.cog_slash(**params["give"])
async def give(self, ctx, user, amount):
"""Give another user an amount of GwendoBucks."""
await self.bot.money.giveMoney(ctx, user, amount)
@cog_ext.cog_slash(**params["invest"])
async def invest(self, ctx, parameters="check"):
"""Invest GwendoBucks in the stock market."""
await self.bot.games.invest.parseInvest(ctx, parameters)
@cog_ext.cog_slash(**params["trivia"])
async def trivia(self, ctx, answer=""):
"""Run a game of trivia."""
await self.bot.games.trivia.triviaParse(ctx, answer)
class BlackjackCog(commands.Cog):
"""Contains the blackjack commands."""
def __init__(self, bot):
"""Initialize the cog."""
self.bot = bot
@cog_ext.cog_subcommand(**params["blackjack_start"])
async def blackjack_start(self, ctx):
"""Start a game of blackjack."""
await self.bot.games.blackjack.start(ctx)
@cog_ext.cog_subcommand(**params["blackjack_bet"])
async def blackjack_bet(self, ctx, bet):
"""Enter the game of blackjack with a bet."""
await self.bot.games.blackjack.enter_game(ctx, bet)
@cog_ext.cog_subcommand(**params["blackjack_hilo"])
async def blackjack_hilo(self, ctx):
"""Get the hilo value for the deck in blackjack."""
await self.bot.games.blackjack.hilo(ctx)
@cog_ext.cog_subcommand(**params["blackjack_shuffle"])
async def blackjack_shuffle(self, ctx):
"""Shuffle the blackjack game."""
await self.bot.games.blackjack.shuffle(ctx)
@cog_ext.cog_subcommand(**params["blackjack_cards"])
async def blackjack_cards(self, ctx):
"""Get the amount of cards left in the blackjack deck."""
await self.bot.games.blackjack.cards(ctx)
class ConnectFourCog(commands.Cog):
"""Contains all the connect four commands."""
def __init__(self, bot):
"""Initialize the cog."""
self.bot = bot
@cog_ext.cog_subcommand(**params["connect_four_start_user"])
async def connect_four_start_user(self, ctx, user):
"""Start a game of connect four against another user."""
await self.bot.games.connect_four.start(ctx, user)
@cog_ext.cog_subcommand(**params["connect_four_start_gwendolyn"])
async def connect_four_start_gwendolyn(self, ctx, difficulty=3):
"""Start a game of connect four against Gwendolyn."""
await self.bot.games.connect_four.start(ctx, difficulty)
class HangmanCog(commands.Cog):
"""Contains all the hangman commands."""
def __init__(self, bot):
"""Initialize the cog."""
self.bot = bot
@cog_ext.cog_slash(**params["hangman"])
async def hangman(self, ctx):
"""Start a game of hangman."""
await self.bot.games.hangman.start(ctx)
class WordleCog(commands.Cog):
"""Contains all the wordle commands."""
def __init__(self, bot):
"""Initialize the cog."""
self.bot = bot
@cog_ext.cog_subcommand(**params["wordle_start"])
async def wordle_start(self, ctx, letters = 5):
"""Start a game of wordle."""
await self.bot.games.wordle.start(ctx, letters)
@cog_ext.cog_subcommand(**params["wordle_guess"])
async def wordle_guess(self, ctx, guess):
"""Start a game of wordle."""
await self.bot.games.wordle.guess(ctx, guess)
class HexCog(commands.Cog):
"""Contains all the hex commands."""
def __init__(self, bot):
"""Initialize the cog."""
self.bot = bot
self.hex = self.bot.games.hex
@cog_ext.cog_subcommand(**params["hex_start_user"])
async def hex_start_user(self, ctx, user):
"""Start a game of hex against another player."""
await self.hex.start(ctx, user)
@cog_ext.cog_subcommand(**params["hex_start_gwendolyn"])
async def hex_start_gwendolyn(self, ctx, difficulty=2):
"""Start a game of hex against Gwendolyn."""
await self.hex.start(ctx, difficulty)
@cog_ext.cog_subcommand(**params["hex_place"])
async def hex_place(self, ctx, coordinates):
"""Place a piece in the hex game."""
await self.hex.placeHex(ctx, coordinates, f"#{ctx.author.id}")
@cog_ext.cog_subcommand(**params["hex_undo"])
async def hex_undo(self, ctx):
"""Undo your last hex move."""
await self.hex.undo(ctx)
@cog_ext.cog_subcommand(**params["hex_swap"])
async def hex_swap(self, ctx):
"""Perform a hex swap."""
await self.hex.swap(ctx)
@cog_ext.cog_subcommand(**params["hex_surrender"])
async def hex_surrender(self, ctx):
"""Surrender the hex game."""
await self.hex.surrender(ctx)
def setup(bot):
"""Add all the cogs to the bot."""
bot.add_cog(GamesCog(bot))
bot.add_cog(BlackjackCog(bot))
bot.add_cog(ConnectFourCog(bot))
bot.add_cog(HangmanCog(bot))
bot.add_cog(HexCog(bot))
bot.add_cog(WordleCog(bot))

View File

@@ -1,32 +0,0 @@
"""Contains the LookupCog, which deals with the lookup commands."""
from discord.ext import commands # Has the cog class
from discord_slash import cog_ext # Used for slash commands
from gwendolyn.utils import get_params # pylint: disable=import-error
params = get_params()
class LookupCog(commands.Cog):
"""Contains the lookup commands."""
def __init__(self, bot):
"""Initialize the cog."""
self.bot = bot
# Looks up a spell
@cog_ext.cog_slash(**params["spell"])
async def spell(self, ctx, query):
"""Look up a spell."""
await self.bot.lookup_funcs.spell_func(ctx, query)
# Looks up a monster
@cog_ext.cog_slash(**params["monster"])
async def monster(self, ctx, query):
"""Look up a monster."""
await self.bot.lookup_funcs.monster_func(ctx, query)
def setup(bot):
"""Add the cog to the bot."""
bot.add_cog(LookupCog(bot))

View File

@@ -1,100 +0,0 @@
"""Contains the MiscCog, which deals with miscellaneous commands."""
from discord.ext import commands # Has the cog class
from discord_slash import cog_ext # Used for slash commands
from discord_slash.context import SlashContext
from gwendolyn.utils import get_params # pylint: disable=import-error
params = get_params()
class MiscCog(commands.Cog):
"""Contains the miscellaneous commands."""
def __init__(self, bot):
"""Initialize the cog."""
self.bot = bot
self.bot.remove_command("help")
self.generators = bot.other.generators
self.plex = bot.other.plex
self.nerd_shit = bot.other.nerd_shit
@cog_ext.cog_slash(**params["ping"])
async def ping(self, ctx: SlashContext):
"""Send the bot's latency."""
await ctx.send(f"Pong!\nLatency is {round(self.bot.latency * 1000)}ms")
@cog_ext.cog_slash(**params["stop"])
async def stop(self, ctx: SlashContext):
"""Stop the bot."""
await self.bot.stop(ctx)
@cog_ext.cog_slash(**params["help"])
async def help_command(self, ctx: SlashContext, command=""):
"""Get help for commands."""
await self.bot.other.help_func(ctx, command)
@cog_ext.cog_slash(**params["thank"])
async def thank(self, ctx: SlashContext):
"""Thank the bot."""
await ctx.send("You're welcome :blush:")
@cog_ext.cog_slash(**params["hello"])
async def hello(self, ctx: SlashContext):
"""Greet the bot."""
await self.bot.other.hello_func(ctx)
@cog_ext.cog_slash(**params["roll"])
async def roll(self, ctx: SlashContext, dice="1d20"):
"""Roll dice."""
await self.bot.other.roll_dice(ctx, dice)
@cog_ext.cog_slash(**params["image"])
async def image(self, ctx: SlashContext):
"""Get a random image from Bing."""
await self.bot.other.image_func(ctx)
@cog_ext.cog_slash(**params["movie"])
async def movie(self, ctx: SlashContext):
"""Get a random movie from the Plex server."""
await self.bot.other.movie_func(ctx)
@cog_ext.cog_slash(**params["name"])
async def name(self, ctx: SlashContext):
"""Generate a random name."""
await self.generators.name_gen(ctx)
@cog_ext.cog_slash(**params["tavern"])
async def tavern(self, ctx: SlashContext):
"""Generate a random tavern name."""
await self.generators.tavern_gen(ctx)
@cog_ext.cog_slash(**params["wiki"])
async def wiki(self, ctx: SlashContext, wiki_page=""):
"""Get a page on a fandom wiki."""
await self.bot.other.find_wiki_page(ctx, wiki_page)
@cog_ext.cog_slash(**params["add_movie"])
async def add_movie(self, ctx: SlashContext, movie):
"""Search for a movie and add it to the Plex server."""
await self.plex.request_movie(ctx, movie)
@cog_ext.cog_slash(**params["add_show"])
async def add_show(self, ctx: SlashContext, show):
"""Search for a show and add it to the Plex server."""
await self.plex.request_show(ctx, show)
@cog_ext.cog_slash(**params["downloading"])
async def downloading(self, ctx: SlashContext, parameters="-d"):
"""Get the current downloading torrents."""
await self.plex.downloading(ctx, parameters)
@cog_ext.cog_slash(**params["wolf"])
async def wolf(self, ctx: SlashContext, query):
"""Perform a search on Wolfram Alpha."""
await self.nerd_shit.wolf_search(ctx, query)
def setup(bot):
"""Add the cog to the bot."""
bot.add_cog(MiscCog(bot))

View File

@@ -1,40 +0,0 @@
"""Contains the StarWarsCog, which deals with Star Wars commands."""
from discord.ext import commands
from discord_slash import cog_ext
from gwendolyn.utils import get_params # pylint: disable=import-error
params = get_params()
class StarWarsCog(commands.Cog):
"""Contains the Star Wars commands."""
def __init__(self, bot):
"""Initialize the cog."""
self.bot = bot
@cog_ext.cog_slash(**params["star_wars_roll"])
async def star_wars_roll(self, ctx, dice=""):
"""Roll Star Wars dice."""
await self.bot.star_wars.roll.parseRoll(ctx, dice)
@cog_ext.cog_slash(**params["star_wars_destiny"])
async def star_wars_destiny(self, ctx, parameters=""):
"""Control Star Wars destiny points."""
await self.bot.star_wars.destiny.parseDestiny(ctx, parameters)
@cog_ext.cog_slash(**params["star_wars_crit"])
async def star_wars_crit(self, ctx, severity: int = 0):
"""Roll for critical injuries."""
await self.bot.star_wars.roll.critRoll(ctx, severity)
@cog_ext.cog_slash(**params["star_wars_character"])
async def star_wars_character(self, ctx, parameters=""):
"""Access and change Star Wars character sheet data."""
await self.bot.star_wars.character.parseChar(ctx, parameters)
def setup(bot):
"""Add the cog to the bot."""
bot.add_cog(StarWarsCog(bot))

View File

@@ -1,11 +1,14 @@
"""Exceptions for Gwendolyn""" class NoToken(Exception):
def __init__(self) -> None:
class GameNotInDatabase(Exception): self.message = "No discord bot token has been set in the .env file"
def __init__(self, game: str, channel: str):
self.message = f"There is no {game} game in channel {channel}"
super().__init__(self.message) super().__init__(self.message)
class InvalidInteraction(Exception): class CannotConnectToService(Exception):
def __init__(self, custom_id: str, decoded: str): def __init__(self, service: str) -> None:
self.message = f"{custom_id = }, {decoded = }" self.message = f"Cannot connect to {service}"
super().__init__(self.message)
class ServiceNotProvided(Exception):
def __init__(self, service: str) -> None:
self.message = f"A {service} instance was not provided"
super().__init__(self.message) super().__init__(self.message)

View File

@@ -0,0 +1,16 @@
from interactions import Extension, slash_command, SlashContext
from gwendolyn.utils import PARAMS as params
class BetterNetflixExtension(Extension):
"""Contains the Better Netflix commands."""
@slash_command(**params["better_netflix"]["movie"])
async def movie(self, ctx: SlashContext):
await self.bot.better_netflix.movie(ctx)
@slash_command(**params["better_netflix"]["show"])
async def show(self, ctx: SlashContext):
await self.bot.better_netflix.show(ctx)
@slash_command(**params["better_netflix"]["downloading"])
async def downloading(self, ctx: SlashContext):
await self.bot.better_netflix.downloading(ctx)

21
gwendolyn/ext/events.py Normal file
View File

@@ -0,0 +1,21 @@
from interactions import Extension, listen, Status, Activity, ActivityType
from interactions.api.events import Startup, Error
class EventExtension(Extension):
"""Listens to miscellaneous events."""
@listen(Startup)
async def startup(self, _):
"""Log and sets status when it logs in."""
name = self.bot.user.username
userid = str(self.bot.user.id)
logged_in_message = f"Logged in as {name}, {userid}"
self.bot.log(logged_in_message, level=25)
command_list = ",".join([str(i.name) for i in self.bot.application_commands])
self.bot.log(f"Commands: {command_list}")
activity = Activity("custom", ActivityType.CUSTOM, state="Type `/help` for commands")
await self.bot.change_presence(activity=activity, status=Status.ONLINE)
@listen(Error)
async def error(self, err: Error):
self.bot.log(f"Error at {err.source}", level=25)

23
gwendolyn/ext/games.py Normal file
View File

@@ -0,0 +1,23 @@
import typing
from interactions import Extension, slash_command, SlashContext, User, Client
from gwendolyn.utils import PARAMS as params
if typing.TYPE_CHECKING:
from gwendolyn_client import Gwendolyn
class GamesExtension(Extension):
"""Contains the game commands."""
def dummy(self):
# For type checking:
self.bot: "Gwendolyn"
@slash_command(**params["games"]["money"]["balance"])
async def balance(self, ctx: SlashContext):
await self.bot.money.sendBalance(ctx)
@slash_command(**params["games"]["money"]["give"])
async def give(self, ctx: SlashContext, user: User, amount: int):
await self.bot.money.giveMoney(ctx, user, amount)

56
gwendolyn/ext/misc.py Normal file
View File

@@ -0,0 +1,56 @@
from interactions import Extension, slash_command, SlashContext, Status
from gwendolyn.utils import PARAMS as params
class MiscExtension(Extension):
"""Contains the miscellaneous commands."""
@slash_command(**params["misc"]["gen_name"])
async def gen_name(self, ctx: SlashContext):
await self.bot.other.generate_name(ctx)
@slash_command(**params["misc"]["hello"])
async def hello(self, ctx: SlashContext):
"""Greet the bot."""
await self.bot.other.hello_func(ctx)
@slash_command(**params["misc"]["help"])
async def help(self, ctx: SlashContext, command=""):
"""Get help for commands."""
await self.bot.other.help_func(ctx, command)
@slash_command(**params["misc"]["ping"])
async def ping(self, ctx: SlashContext):
"""Send the bot's latency."""
latency = self.bot.latency
if latency > 100:
await ctx.send("Cannot measure latency :(")
else:
await ctx.send(f"Pong!\nLatency is {round(self.bot.latency * 1000)}ms")
@slash_command(**params["misc"]["roll"])
async def roll(self, ctx: SlashContext, dice: str = "1d20"):
await self.bot.other.roll_dice(ctx, dice)
@slash_command(**params["misc"]["thank"])
async def thank(self, ctx: SlashContext):
"""Thank the bot."""
await ctx.send("You're welcome :blush:")
@slash_command(**params["misc"]["stop"])
async def stop(self, ctx: SlashContext):
if f"{ctx.author.id}" in self.bot.admins:
await ctx.send("Goodnight...")
await self.bot.change_presence(status=Status.INVISIBLE)
# self.bot.database_funcs.wipe_games()
self.bot.log("Logging out", level=25)
await self.bot.stop()
else:
log_message = f"{ctx.author.display_name} tried to stop me!"
self.bot.log(log_message, str(ctx.channel_id))
await ctx.send(f"I don't think I will, {ctx.author.display_name}")
@slash_command(**params["misc"]["echo"])
async def echo(self, ctx: SlashContext, text: str):
await ctx.send(text)

View File

@@ -1,11 +1,7 @@
"""A collection of all Gwendolyn functions.""" """A collection of all Gwendolyn functions."""
__all__ = ["Games", "Money", "LookupFuncs", "StarWars"] __all__ = ["Other","BetterNetflix", "Sonarr", "Radarr", "TMDb", "QBittorrent", "Money"]
from .games import Money, Games
from .lookup import LookupFuncs
from .other import Other from .other import Other
from .better_netflix import Radarr, Sonarr, BetterNetflix, TMDb, QBittorrent
from .star_wars_funcs import StarWars from .games import Money, Games

View File

@@ -0,0 +1,5 @@
"""Better Netflix functions for Gwendolyn."""
__all__ = ["BetterNetflix", "Sonarr", "Radarr", "TMDb", "QBittorrent"]
from .better_netflix import BetterNetflix, Sonarr, Radarr, TMDb, QBittorrent

View File

@@ -0,0 +1,304 @@
from requests import get, post
from random import choice
import io
from math import floor
from time import time
from PIL import Image
from interactions import SlashContext, Embed, EmbedFooter, EmbedAttachment, File
from gwendolyn.exceptions import CannotConnectToService, ServiceNotProvided
class Service():
def __init__(self, ip: str, port: str, api_key: str) -> None:
self.ip = ip
self.port = port
self.header = {
"accept": "application/json",
"X-Api-Key": api_key
}
def test(self):
return get(f"http://{self.ip}:{self.port}/api", headers=self.header).ok
class Radarr(Service):
def movies(self):
response = get(f"http://{self.ip}:{self.port}/api/v3/movie", headers=self.header)
if not response.ok:
return []
return [m for m in response.json() if m["hasFile"]]
class Sonarr(Service):
def shows(self):
response = get(f"http://{self.ip}:{self.port}/api/v3/series", headers=self.header)
if not response.ok:
return []
return response.json()
class QBittorrent(Service):
def __init__(self, ip: str, port: str, username: str, password: str) -> None:
self.ip = ip
self.port = port
data = {"username": username,"password": password}
response = post(f"http://{ip}:{port}/api/v2/auth/login", data=data)
self.logged_in = response.ok
self.cookie = {"SID": response.cookies.values()[0]}
def test(self):
return self.logged_in
def get_torrents(self):
response = get(
f"http://{self.ip}:{self.port}/api/v2/torrents/info",
cookies=self.cookie
)
return response.json()
class TMDb():
def __init__(self, api_token: str) -> None:
self.header = {
"accept": "application/json",
"Authorization": "Bearer "+api_token
}
def test(self):
return get(f"https://api.themoviedb.org/3/account", headers=self.header).ok
class BetterNetflix():
def __init__(self, bot, **kwargs) -> None:
self.bot = bot
services = ["radarr","sonarr","tmdb","qbittorrent"]
for service in services:
try:
setattr(self,service,kwargs[service])
except:
raise ServiceNotProvided(service)
if getattr(self,service).test():
self.bot.log(f"Connected to {service}")
else:
raise CannotConnectToService(service)
def _poster_color(self, poster_url: str):
response = get(poster_url, stream=True)
img = Image.open(io.BytesIO(response.content))
img.thumbnail((150, 100))
# Reduce colors (uses k-means internally)
paletted = img.convert('P', palette=Image.ADAPTIVE, colors=6)
buf = io.BytesIO()
img2 = paletted.convert("RGB")
img2.save("gwendolyn/resources/temp.jpg")
# Find the color that occurs most often
palette = paletted.getpalette()
color_counts = sorted(paletted.getcolors(), reverse=True)
palette_index = color_counts[0][1]
dominant_color = palette[palette_index*3:palette_index*3+3]
return dominant_color
async def movie(self, ctx: SlashContext):
msg = await ctx.send("Finding a random movie...")
movies = self.radarr.movies()
if len(movies) == 0:
await msg.edit(content="Unable to find any movies")
return
picked_movie = choice(movies)
description = ""
if "imdb" in picked_movie['ratings']:
description += f"<:imdb:1301506320603676782> {picked_movie['ratings']['imdb']['value']}"
description += "/10 "
if "rottenTomatoes" in picked_movie['ratings']:
rt_value = picked_movie['ratings']['rottenTomatoes']['value']
rt_icon = "<:fresh:1301509701338660894>" if rt_value >= 60 else "<:rotten:1301509685907820607>"
description += f"{rt_icon} {rt_value}%"
if description != "":
description += "\n\n"
year = EmbedFooter(str(picked_movie["year"])) if "year" in picked_movie else None
has_poster = len(picked_movie["images"]) > 0 and "remoteUrl" in picked_movie["images"][0]
poster = EmbedAttachment(picked_movie["images"][0]["remoteUrl"]) if has_poster else None
color = self._poster_color(picked_movie["images"][0]["remoteUrl"]) if has_poster else "#ff0000"
description += picked_movie["overview"]
embed = Embed(
picked_movie["title"],
description,
color,
thumbnail=poster,
footer=year
)
await msg.edit(content="",embed=embed)
async def show(self, ctx: SlashContext):
msg = await ctx.send("Finding a random show...")
shows = self.sonarr.shows()
if len(shows) == 0:
await msg.edit(content="Unable to find any shows")
return
picked_show = choice(shows)
description = ""
if "imdb" in picked_show['ratings']:
description += f"<:imdb:1301506320603676782> {picked_show['ratings']['imdb']['value']}"
description += "\u1CBC\u1CBC"
if "rottenTomatoes" in picked_show['ratings']:
rt_value = picked_show['ratings']['rottenTomatoes']['value']
rt_icon = "<:fresh:1301509701338660894>" if rt_value >= 60 else "<:rotten:1301509685907820607>"
description += f"{rt_icon} {rt_value}%"
if description != "":
description += "\n\n"
year = EmbedFooter(str(picked_show["year"])) if "year" in picked_show else None
images = {i["coverType"]:i for i in picked_show["images"]}
has_poster = "poster" in images and "remoteUrl" in images["poster"]
poster = EmbedAttachment(images["poster"]["remoteUrl"]) if has_poster else None
color = self._poster_color(images["poster"]["remoteUrl"]) if has_poster else "#ff0000"
description += picked_show["overview"]
embed = Embed(
picked_show["title"],
description,
color,
thumbnail=poster,
footer=year
)
await msg.edit(content="",embed=embed)
def _draw_torrent_list(self, title_width: int):
def ratio_to_bar(ratio):
progress_bar = "|"+(""*floor(download_ratio*20))
while len(progress_bar) < 21:
progress_bar += " "
progress_bar += "| "+str(floor(download_ratio*100))+"%"
while len(progress_bar) < 27:
progress_bar += " "
return progress_bar
message = []
all_downloaded = True
dm_section_title = "*Torrent Downloads*"
dm_section_title_line = "-"*((title_width-len(dm_section_title))//2)
message.append(
dm_section_title_line+dm_section_title+dm_section_title_line
)
torrent_list = self.qbittorrent.get_torrents()
if len(torrent_list) == 0:
message.append("No torrents currently downloading")
return message, all_downloaded
for torrent in torrent_list:
if torrent['category'] not in ["radarr", "tv-sonarr"]:
break
torrent_name = torrent["name"]
if len(torrent_name) > 30:
if torrent_name[26] == " ":
torrent_name = torrent_name[:26]+"...."
else:
torrent_name = torrent_name[:27]+"..."
while len(torrent_name) < 30:
torrent_name += " "
if torrent["size"] == 0:
download_ratio = 0
elif torrent["amount_left"] == 0:
download_ratio = 1
else:
download_ratio = min(
torrent["downloaded"]/torrent["size"],
1
)
progress_bar = ratio_to_bar(download_ratio)
eta_in_seconds = torrent["eta"]
if eta_in_seconds >= 8640000:
eta = ""
else:
eta = ""
if eta_in_seconds >= 86400:
eta += str(floor(eta_in_seconds/86400))+"d "
if eta_in_seconds >= 3600:
eta += str(floor((eta_in_seconds%86400)/3600))+"h "
if eta_in_seconds >= 60:
eta += str(floor((eta_in_seconds%3600)/60))+"m "
eta += str(eta_in_seconds%60)+"s"
torrent_info = f"{torrent_name} {progress_bar} "
torrent_info += f"(Eta: {eta})"
if torrent["state"] == "stalledDL":
torrent_info += " (Stalled)"
if not (download_ratio == 1 and
torrent["last_activity"] < time()-7200):
message.append(torrent_info)
return message
async def _generate_download_list(self):
"""Generate a list of all torrents.
*Returns*
message_text: str
A formatted list of all torrents
"""
self.bot.log("Generating torrent list")
title_width = 90
message = self._draw_torrent_list(title_width)
message_text = "```"+"\n".join(message)+"```"
if message_text == "``````":
message_text = self.long_strings["No torrents downloading"]
return message_text
async def downloading(self, ctx: SlashContext):
"""Send message with list of all downloading torrents."""
async def send_long_message(ctx,message_text):
if len(message_text) <= 1994:
await ctx.send("```"+message_text+"```")
else:
cut_off_index = message_text[:1994].rfind("\n")
await ctx.send("```"+message_text[:cut_off_index]+"```")
await send_long_message(ctx,message_text[cut_off_index+1:])
await ctx.defer()
message_text = await self._generate_download_list()
if not message_text.startswith("```"):
await ctx.send(message_text)
elif len(message_text) > 2000:
message_text = message_text[3:-3]
await send_long_message(ctx,message_text)
else:
await ctx.send(message_text)

View File

@@ -1,6 +1,11 @@
"""Functions for games Gwendolyn can play.""" """Game functions for Gwendolyn."""
__all__ = ["Money", "Games"] __all__ = ["Money", "Games"]
from .money import Money from .money import Money
from .games_container import Games
class Games():
def __init__(self, bot):
"""Initialize the class."""
self.bot = bot
self.database = bot.database

View File

@@ -1,966 +0,0 @@
"""
Runs commands, game logic and imaging for blackjack games.
*Classes*
---------
Blackjack
Contains the blackjack game logic.
DrawBlackjack
Draws images of the blackjack table.
"""
import math # Used for flooring decimal numbers
import datetime # Used to generate the game id
import asyncio # Used for sleeping
from discord_slash.context import InteractionContext as IntCont # Used for
# typehints
from discord_slash.context import ComponentContext
from discord.abc import Messageable
from PIL import Image
from gwendolyn.utils import replace_multiple
from .game_base import CardGame, CardDrawer
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
RED = (255, 50, 50)
GOLD = (155, 123, 0)
def _is_round_done(game: dict):
"""
Find out if the round is done.
*Parameters*
------------
game: dict
The game to check.
*Returns*
---------
round_done: bool
Whether the round is done.
"""
return all(
hand["hit"] or hand["standing"]
for user_hands in game["user hands"].values() for hand in user_hands
)
class Blackjack(CardGame):
"""
Deals with blackjack commands and gameplay logic.
*Methods*
---------
hit(ctx: IntCont, hand_number: int = 0)
double(ctx: IntCont, hand_number: int = 0)
stand(ctx: IntCont, hand_number: int = 0)
split(ctx: IntCont, hand_number: int = 0)
enter_game(ctx: IntCont, bet: int)
start(ctx: IntCont)
hilo(ctx: IntCont)
shuffle(ctx: IntCont)
cards(ctx: IntCont)
"""
def __init__(self, bot):
"""Initialize the class."""
super().__init__(bot, "blackjack", DrawBlackjack, 4)
default_buttons = ["Hit", "Stand", "Double", "Split"]
self.default_buttons = [(i, [i, "0"], 1) for i in default_buttons]
async def _test_command(self, game: dict, user: str, command: str,
hand_number: int, ctx: IntCont): #pylint:disable=too-many-arguments
valid_command = False
if user not in game["user hands"]:
self.bot.log(f"They tried to {command} without being in the game")
send_msg = f"You have to enter the game before you can {command}"
elif len(game["user hands"][user]) < hand_number:
self.bot.log(f"They tried to {command} with a hand they don't have")
send_msg = "You don't have that many hands"
elif game["round"] <= 0:
self.bot.log(f"They tried to {command} on the 0th round")
send_msg = "You haven't seen your cards yet!"
elif len(game["user hands"][user]) > 1 and hand_number == 0:
self._get_hand_number(ctx, command, game["user hands"][user])
return False
elif game["user hands"][user][max(hand_number-1,0)]["hit"]:
self.bot.log("They've already hit this round")
send_msg = "You've already hit this round"
elif game["user hands"][user][max(hand_number-1,0)]["standing"]:
self.bot.log("They're already standing")
send_msg = "You're already standing"
else:
valid_command = True
if not valid_command:
await ctx.send(send_msg, hidden=True)
return valid_command
def _shuffle_cards(self, channel: str):
"""
Shuffle an amount of decks equal to self.decks_used.
The shuffled cards are placed in the database as the cards that
are used by other blackjack functions.
*Parameters*
------------
channel: str
The id of the channel where the cards will be used.
"""
super()._shuffle_cards(channel)
# Creates hilo file
self.bot.log(f"creating hilo doc for {channel}")
hilo_updater = {"$set": {"_id": channel, "hilo": 0}}
self._update_document(channel, hilo_updater, "hilo")
def _calc_hand_value(self, hand: list):
"""
Calculate the value of a blackjack hand.
*Parameters*
------------
hand: list
The hand to calculate the value of. Each element in the
list is a card represented as a string in the format of
"xy" where x is the number of the card (0, k, q, and j
for 10s, kings, queens and jacks) and y is the suit (d,
c, h or s).
*Returns*
---------
hand_value: int
The blackjack value of the hand.
"""
values = [0]
for card in hand:
card_value = replace_multiple(card[0], ["0", "k", "q", "j"], "10")
if card_value == "a":
values = [i + 1 for i in values] + [i + 11 for i in values]
else:
values = [i+int(card_value) for i in values]
valid_values = [i for i in values if i <= 21]
hand_value = max(valid_values) if valid_values else min(values)
self.bot.log(f"Calculated the value of {hand} to be {hand_value}")
return hand_value
def _draw_card(self, channel: str):
drawn_card = super()._draw_card(channel)
hilo_value = min(1,-1-((self._calc_hand_value([drawn_card])-10)//3))
self._update_document(channel, {"$inc": {"hilo": hilo_value}}, "hilo")
return drawn_card
def _dealer_draw(self, channel: str):
"""
Draw a card for the dealer.
*Parameters*
------------
channel: str
The id of the channel to draw a card for the dealer in.
*Returns*
---------
done: bool
Whether the dealer is done drawing cards.
"""
game = self.access_document(channel)
dealer_hand = game["dealer hand"]
if self._calc_hand_value(dealer_hand) < 17:
dealer_hand.append(self._draw_card(channel))
dealer_updater = {"$set": {"dealer hand": dealer_hand}}
self._update_document(channel, dealer_updater)
done = False
else:
done = True
if self._calc_hand_value(dealer_hand) > 21:
self._update_document(channel, {"$set": {"dealer busted": True}})
return done
def _blackjack_continue(self, channel: str):
"""
Continues the blackjack game to the next round.
*Parameters*
------------
channel: str
The id of the channel the blackjack game is in
*Returns*
---------
send_message: str
The message to send to the channel.
all_standing: bool
If all players are standing.
gameDone: bool
If the game has finished.
"""
self.bot.log("Continuing blackjack game")
game = self.access_document(channel)
self._update_document(channel, {"$inc": {"round": 1}})
message = self.long_strings["Blackjack all players standing"]
all_standing = True
pre_all_standing = True
done = False
if game["all standing"]:
self.bot.log("All are standing")
done = self._dealer_draw(channel)
message = "The dealer draws a card."
self.bot.log("Testing if all are standing")
for user, user_hands in game["user hands"].items():
standing_test = (self._test_if_standing(user_hands))
hand_updater = {"$set": {"user hands."+user: standing_test[0]}}
self._update_document(channel, hand_updater)
if not standing_test[1]:
all_standing = False
if not standing_test[2]:
pre_all_standing = False
if all_standing:
self._update_document(channel, {"$set": {"all standing": True}})
if done:
message = "The dealer is done drawing cards"
return message, True, done
if pre_all_standing:
return "", True, done
send_message = self.long_strings["Blackjack commands"]
if game["round"] == 0:
send_message = send_message.format(
self.long_strings["Blackjack first round"])
else:
send_message = send_message.format("")
return send_message, False, done
def _test_if_standing(self, user_hands: list):
"""
Test if a player is standing on all their hands.
Also resets the hand if it's not standing
*Parameters*
------------
hand: dict
The hands to test and reset.
*Returns*
---------
hand: dict
The reset hand.
all_standing: bool
If the player is standing on all their hands.
pre_all_standing: bool
Is true if all_standing is True, or if a player has done
something equivalent to standing but still needs to see
the newly drawn card.
"""
# If the user has not hit, they are by definition standing.
all_standing = True
pre_all_standing = True
for hand in user_hands:
if not hand["hit"]:
hand["standing"] = True
if not hand["standing"]:
all_standing = False
if self._calc_hand_value(hand["hand"]) >= 21 or hand["doubled"]:
hand["standing"] = True
else:
pre_all_standing = False
hand["hit"] = False
return user_hands, all_standing, pre_all_standing
async def _blackjack_finish(self, channel: Messageable):
"""
Generate the winnings message after the blackjack game ends.
*Parameters*
------------
channel: str
The id of the channel the game is in.
*Returns*
---------
final_winnings: str
The winnings message.
"""
final_winnings = "*Final Winnings:*\n"
game = self.access_document(str(channel.id))
for user in game["user hands"]:
winnings, net_winnings, reason = self._calc_winning(
game["user hands"][user],
self._calc_hand_value(game["dealer hand"]),
game["dealer blackjack"],
game["dealer busted"]
)
user_name = self.bot.database_funcs.get_name(user)
if winnings < 0:
final_winnings += f"{user_name} lost"
else:
final_winnings += f"{user_name} won"
if abs(winnings) == 1:
final_winnings += " 1 GwendoBuck"
else:
final_winnings += f" {abs(winnings)} GwendoBucks"
final_winnings += f" {reason}\n"
self.bot.money.addMoney(user, net_winnings)
await self._end_game(channel)
return final_winnings
def _calc_winning(self, user_hands: dict, dealer_value: int,
dealer_blackjack: bool, dealer_busted: bool):
"""
Calculate how much a user has won/lost in the blackjack game.
*Parameters*
------------
hand: dict
The hand to calculate the winnings of.
dealer_value: int
The dealer's hand value.
top_level: bool
If the input hand is _all_ if the player's hands. If
False, it's one of the hands resulting from a split.
dealer_blackjack: bool
If the dealer has a blackjack.
dealer_busted: bool
If the dealer busted.
*Returns*
---------
winnings: int
How much the player has won/lost.
net_winnings: int
winnings minus the original bet. This is added to the
user's account, since the bet was removed from their
account when they placed the bet.
reason: str
The reason for why they won/lost.
"""
self.bot.log("Calculating winnings")
reason = ""
winnings = 0
net_winnings = 0
for hand in user_hands:
winnings += -1 * hand["bet"]
hand_value = self._calc_hand_value(hand["hand"])
states = [
(dealer_blackjack, "dealer blackjack", 0),
(hand["blackjack"], "blackjack", math.floor(2.5 * hand["bet"])),
(hand["busted"], "busted", 0),
(dealer_busted, dealer_busted, 2*hand["bet"]),
(hand_value == dealer_value, "pushed", hand["bet"]),
(hand_value > dealer_value, "highest value", 2 * hand["bet"]),
(True, "highest value", 0)
]
for state in states:
if state[0]:
reason += f"({state[1]})"
net_winnings += state[2]
break
winnings += net_winnings
return winnings, net_winnings, reason
async def _blackjack_loop(self, channel, game_round: int, game_id: str):
"""
Run blackjack logic and continue if enough time passes.
*Parameters*
------------
channel: guildChannel or DMChannel
The channel the game is happening in.
game_round: int
The round to start.
game_id: str
The ID of the game.
"""
self.bot.log(f"Loop {game_round}", str(channel.id))
new_message, all_standing, game_done = self._blackjack_continue(
str(channel.id)
)
if new_message != "":
self.bot.log(new_message, str(channel.id))
await channel.send(new_message)
if not game_done:
await self._delete_old_image(channel)
buttons = None if all_standing else self.default_buttons
await self._send_image(channel, buttons)
await asyncio.sleep([120,5][all_standing])
if not self._test_document(str(channel.id)):
self.bot.log(f"Ending loop on round {game_round}", str(channel.id))
return
game = self.access_document(str(channel.id))
if game_round != game["round"] or game_id != game["game_id"]:
self.bot.log(f"Ending loop on round {game_round}", str(channel.id))
return
if not game_done:
log_message = f"Loop {game_round} starting a new blackjack loop"
self.bot.log(log_message, str(channel.id))
await self._blackjack_loop(channel, game_round+1, game_id)
else:
await channel.send(await self._blackjack_finish(channel))
async def _get_hand_number(self, ctx: IntCont, command: str,
hands_amount: int):
buttons = [
(str(i+1), [command, "0", str(i+1)], 1) for i in range(hands_amount)
]
await ctx.send(
f"Which hand do you want to {command}?",
hidden=True,
components=self._get_action_rows(buttons)
)
async def _hit(self, ctx: IntCont, hand_number: int = 0):
"""
Hit on a hand.
*Parameters*
------------
ctx: IntCont
The context of the command.
hand_number: int = 1
The number of the hand to hit.
"""
await self.bot.defer(ctx)
channel = str(ctx.channel_id)
user = f"#{ctx.author.id}"
game = self.access_document(channel)
if not await self._test_command(game, user, "hit", hand_number, ctx):
return
hand = game["user hands"][user][max(hand_number-1,0)]
hand["hand"].append(self._draw_card(channel))
hand["hit"] = True
hand_value = self._calc_hand_value(hand["hand"])
if hand_value > 21:
hand["busted"] = True
hand_path = f"user hands.{user}.{max(hand_number-1,0)}"
self._update_document(channel, {"$set": {hand_path: hand}})
game = self.access_document(channel)
await ctx.send(f"{ctx.author.display_name} hit")
self.bot.log("They succeeded")
if _is_round_done(game):
game_id = game["game_id"]
self.bot.log("Hit calling self._blackjack_loop()", channel)
await self._blackjack_loop(
ctx.channel, game["round"]+1, game_id
)
async def _double(self, ctx: IntCont, hand_number: int = 0):
"""
Double a hand.
*Parameters*
------------
ctx: IntCont
The context of the command.
hand_number: int = 0
The number of the hand to double.
"""
await self.bot.defer(ctx)
channel = str(ctx.channel_id)
user = f"#{ctx.author.id}"
game = self.access_document(channel)
balance = self.bot.money.checkBalance(user)
if not await self._test_command(game, user, "double", hand_number, ctx):
return
if len(game["user hands"][user][max(hand_number-1,0)]["hand"]) != 2:
await ctx.send("You can only double on the first round")
self.bot.log("They tried to double after round 1")
elif balance < game["user hands"][user][max(hand_number-1,0)]["bet"]:
await ctx.send("You can't double when you don't have enough money")
self.bot.log("They tried to double without having enough money")
else:
hand = game["user hands"][user][max(hand_number-1,0)]
self.bot.money.addMoney(user, -1 * hand["bet"])
hand["hand"].append(self._draw_card(channel))
hand["hit"] = True
hand["doubled"] = True
hand["bet"] += hand["bet"]
if self._calc_hand_value(hand["hand"]) > 21:
hand["busted"] = True
hand_path = f"user hands.{user}.{max(hand_number-1,0)}"
self._update_document(channel, {"$set": {hand_path: hand}})
game = self.access_document(channel)
user_name = self.bot.database_funcs.get_name(user)
send_message = self.long_strings["Blackjack double"]
await ctx.send(send_message.format(hand["bet"], user_name))
self.bot.log("They succeeded")
if _is_round_done(game):
game_id = game["game_id"]
self.bot.log("Double calling self._blackjack_loop()", channel)
await self._blackjack_loop(
ctx.channel, game["round"]+1, game_id
)
async def _stand(self, ctx: IntCont, hand_number: int = 0):
"""
Stand on a hand.
*Parameters*
------------
ctx: IntCont
The context of the command.
hand_number: int = 0
The number of the hand to stand on.
"""
await self.bot.defer(ctx)
channel = str(ctx.channel_id)
user = f"#{ctx.author.id}"
game = self.access_document(channel)
if not await self._test_command(game, user, "stand", hand_number, ctx):
return
hand = game["user hands"][user][max(hand_number-1,0)]
hand["standing"] = True
hand_path = f"user hands.{user}.{max(hand_number-1,0)}"
self._update_document(channel, {"$set": {hand_path: hand}})
game = self.access_document(channel)
send_message = f"{ctx.author.display_name} is standing"
log_message = "They succeeded"
await ctx.send(send_message)
self.bot.log(log_message)
if _is_round_done(game):
game_id = game["game_id"]
self.bot.log("Stand calling self._blackjack_loop()", channel)
await self._blackjack_loop(ctx.channel, game["round"]+1, game_id)
async def _split(self, ctx: IntCont, hand_number: int = 0):
"""
Split a hand.
*Parameters*
------------
ctx: IntCont
The context of the command.
hand_number: int = 0
The number of the hand to split.
"""
await self.bot.defer(ctx)
channel = str(ctx.channel_id)
user = f"#{ctx.author.id}"
game = self.access_document(channel)
if not await self._test_command(game, user, "split", hand_number, ctx):
return
old_hand = game["user hands"][user][max(hand_number-1,0)]
if len(game["user hands"][user]) > 3:
await ctx.send("You can only split 3 times")
self.bot.log("They tried to split more than three times")
elif len(old_hand["hand"]) != 2:
await ctx.send("You can only split on the first round")
self.bot.log("They tried to split after the first round")
elif len({self._calc_hand_value([i]) for i in old_hand["hand"]}) != 1:
await ctx.send(self.long_strings["Blackjack different cards"])
self.bot.log("They tried to split two different cards")
elif self.bot.money.checkBalance(user) < old_hand["bet"]:
await ctx.send("You don't have enough GwendoBucks")
self.bot.log("They didn't have enough GwendoBucks")
else:
self.bot.money.addMoney(user, -1 * old_hand["bet"])
old_hand["hit"] = True
hands = [old_hand, {
"hand": [old_hand["hand"].pop(1), self._draw_card(channel)],
"bet": old_hand["bet"], "standing": False,
"busted": False, "blackjack": False, "hit": True,
"doubled": False
}]
old_hand["hand"].append(self._draw_card(channel))
for i, hand in enumerate(hands):
if self._calc_hand_value(hand["hand"]) > 21:
hands[i]["busted"] = True
elif self._calc_hand_value(hand["hand"]) == 21:
hands[i]["blackjack"] = True
game["user hands"][user][max(hand_number-1,0)] = hands[0]
game["user hands"][user].append(hands[1])
updater = {"$set": {f"user hands.{user}": game["user hands"][user]}}
self._update_document(channel, updater)
send_message = self.long_strings["Blackjack split"]
user_name = self.bot.database_funcs.get_name(user)
await ctx.send(send_message.format(user_name))
self.bot.log("They succeeded")
game = self.access_document(channel)
if _is_round_done(game):
game_id = game["game_id"]
self.bot.log("Split calling self._blackjack_loop()", channel)
await self._blackjack_loop(
ctx.channel, game["round"]+1, game_id
)
async def enter_game(self, ctx: IntCont, bet: int):
"""
Enter the blackjack game.
*Parameters*
------------
ctx: IntCont
The context of the command.
bet: int
The bet to enter with.
"""
await self.bot.defer(ctx)
channel = str(ctx.channel_id)
user = f"#{ctx.author.id}"
game = self.access_document(channel)
user_name = self.bot.database_funcs.get_name(user)
self.bot.log(f"{user_name} is trying to join the Blackjack game")
if user in game["user hands"]:
await ctx.send("You're already in the game!")
self.bot.log("They're already in the game")
elif len(game["user hands"]) >= 5:
await ctx.send("There can't be more than 5 players in a game")
self.bot.log("There were already 5 players in the game")
elif game["round"] != 0:
await ctx.send("The table is no longer taking bets")
self.bot.log("They tried to join after the game begun")
elif bet < 0:
await ctx.send("You can't bet a negative amount")
self.bot.log("They tried to bet a negative amount")
elif self.bot.money.checkBalance(user) < bet:
await ctx.send("You don't have enough GwendoBucks")
self.bot.log("They didn't have enough GwendoBucks")
else:
self.bot.money.addMoney(user, -1 * bet)
player_hand = [self._draw_card(channel) for _ in range(2)]
new_hand = [{
"hand": player_hand, "bet": bet, "standing": False,
"busted": False,
"blackjack": self._calc_hand_value(player_hand) == 21,
"hit": True, "doubled": False
}]
updater = {"$set": {f"user hands.{user}": new_hand}}
self._update_document(channel, updater)
send_message = "{} entered the game with a bet of {} GwendoBucks"
await ctx.send(send_message.format(user_name, bet))
self.bot.log(send_message.format(user_name, bet))
async def start(self, ctx: IntCont):
"""
Start a blackjack game.
*Parameters*
------------
ctx: IntCont
The context of the command.
"""
await self.bot.defer(ctx)
channel = str(ctx.channel_id)
blackjack_min_cards = 50
self.bot.log("Starting blackjack game")
await ctx.send("Starting a new game of blackjack")
cards_left = 0
if self._test_document(channel, "cards"):
cards = self.access_document(channel, "cards")
cards_left = len(cards["cards"])
# Shuffles if not enough cards
if cards_left < blackjack_min_cards:
self._shuffle_cards(channel)
await ctx.channel.send("Shuffling the deck...")
self.bot.log(f"Trying to start a blackjack game in {channel}")
if self._test_document(channel):
await ctx.channel.send(self.long_strings["Blackjack going on"])
self.bot.log("There was already a game going on")
return
dealer_hand = [self._draw_card(channel), self._draw_card(channel)]
game_id = datetime.datetime.now().strftime('%Y%m%d%H%M%S')
new_game = {
"dealer hand": dealer_hand,
"dealer busted": False, "game_id": game_id,
"dealer blackjack": (self._calc_hand_value(dealer_hand) == 21),
"user hands": {}, "all standing": False, "round": 0
}
await ctx.channel.send(self.long_strings["Blackjack started"])
labels = [0] + [10**i for i in range(4)]
betting_buttons = [
(f"Bet {i}", ["bet", "0", str(i)], 1) for i in labels
]
await self._start_new(ctx.channel, new_game, betting_buttons)
await asyncio.sleep(30)
game = self.access_document(channel)
if len(game["user hands"]) == 0:
await ctx.channel.send("No one entered the game. Ending the game.")
await ctx.channel.send(await self._blackjack_finish(ctx.channel))
return
game_id = game["game_id"]
# Loop of game rounds
self.bot.log("start() calling _blackjack_loop()", channel)
await self._blackjack_loop(ctx.channel, 1, game_id)
async def hilo(self, ctx: IntCont):
"""
Get the hilo of the blackjack game.
*Parameters*
------------
ctx: IntCont
The context of the command.
"""
data = self.access_document(str(ctx.channel_id), "hilo", False)
hilo = data["hilo"] if data else 0
await ctx.send(f"Hi-lo value: {hilo}", hidden=True)
async def shuffle(self, ctx: IntCont):
"""
Shuffle the cards used for blackjack.
*Parameters*
------------
ctx: IntCont
The context of the command.
"""
self._shuffle_cards(str(ctx.channel_id))
await ctx.send("Shuffling the deck...")
async def cards(self, ctx: IntCont):
"""
Get how many cards are left for blackjack.
*Parameters*
------------
ctx: IntCont
The context of the command.
"""
cards = self.access_document(str(ctx.channel_id), "cards", False)
cards_left = len(cards["cards"]) if cards else 0
decks_left = round(cards_left/52, 1) if cards else 0
send_message = f"Cards left:\n{cards_left} cards, {decks_left} decks"
await ctx.send(send_message, hidden=True)
async def decode_interaction(self, ctx: ComponentContext, info: list[str]):
if info[0].lower() == "bet":
await self.enter_game(ctx, int(info[2]))
elif info[0].lower() == "hit":
await self._hit(ctx, int(info[2]) if len(info) > 2 else 0)
elif info[0].lower() == "stand":
await self._stand(ctx, int(info[2]) if len(info) > 2 else 0)
elif info[0].lower() == "double":
await self._double(ctx, int(info[2]) if len(info) > 2 else 0)
elif info[0].lower() == "split":
await self._split(ctx, int(info[2]) if len(info) > 2 else 0)
class DrawBlackjack(CardDrawer):
"""
Draws the blackjack image.
*Methods*
---------
*Attributes*
------------
bot: Gwendolyn
The instance of the bot.
PLACEMENT: list
The order to place the user hands in.
"""
# pylint: disable=too-few-public-methods
def __init__(self, bot, game: Blackjack):
"""Initialize the class."""
super().__init__(bot, game)
# pylint: disable=invalid-name
self.PLACEMENT = [2, 1, 3, 0, 4]
# pylint: enable=invalid-name
self._set_card_size(197)
def _draw_dealer_hand(self, game: dict, table: Image.Image):
dealer_hand = self._draw_hand(
game["dealer hand"],
not game["all standing"],
game["dealer busted"] if game["all standing"] else False,
game["dealer blackjack"] if game["all standing"] else False
)
paste_position = (800-self.CARD_BORDER, 20-self.CARD_BORDER)
table.paste(dealer_hand, paste_position, dealer_hand)
def _draw_player_name(self, user: str, placement: int, table: Image.Image):
user_name = self.bot.database_funcs.get_name(user)
font, font_size = self._adjust_font(50, user_name, 360)
text_width, text_height = font.getsize(user_name)
text_x = 32+(384*placement)+117-(text_width//2)
text_y = 960+text_height
colors = [BLACK, WHITE]
text_image = self._draw_shadow_text(user_name, colors, font_size)
table.paste(text_image, (text_x, text_y), text_image)
def _draw_player_hands(self, all_hands: dict, table: Image.Image):
for i, (user, user_hands) in enumerate(all_hands.items()):
hand_images = []
position_x = 32-self.CARD_BORDER+(384*self.PLACEMENT[i])
for hand in user_hands:
hand_images.append(self._draw_hand(
hand["hand"],
False,
hand["busted"],
hand["blackjack"]
))
for index, image in enumerate(hand_images):
position_y = 840
position_y -= self.CARD_BORDER + (140 * (len(user_hands)-index))
position = (position_x, position_y)
table.paste(image, position, image)
self._draw_player_name(user, self.PLACEMENT[i], table)
def _draw_image(self, game: dict, table: Image.Image):
"""
Draw the table image.
*Parameters*
------------
channel: str
The id of the channel the game is in.
"""
self._draw_dealer_hand(game, table)
self._draw_player_hands(game["user hands"], table)
def _draw_hand(self, hand: dict, dealer: bool, busted: bool,
blackjack: bool):
"""
Draw a hand.
*Parameters*
------------
hand: dict
The hand to draw.
dealer: bool
If the hand belongs to the dealer who hasn't shown
their second card.
busted: bool
If the hand is busted.
blackjack: bool
If the hand has a blackjack.
*Returns*
---------
background: Image
The image of the hand.
"""
self.bot.log("Drawing hand {hand}, {busted}, {blackjack}")
background = self._draw_cards(hand, int(dealer))
if busted or blackjack:
if busted:
text = "BUSTED"
last_color = RED
font_size = 60
elif blackjack:
text = "BLACKJACK"
last_color = GOLD
font_size = 35
colors = [BLACK, WHITE, last_color]
text_image = self._draw_shadow_text(text, colors, font_size)
width = background.size[0]
text_width = self._get_font(font_size).getsize(text)[0]
text_position = (int(width/2)-int(text_width/2), 85)
background.paste(text_image, text_position, text_image)
return background

File diff suppressed because it is too large Load Diff

View File

@@ -1,319 +0,0 @@
"""Base class for the games."""
import random
from typing import Union
from discord import File, User
from discord.abc import Messageable
from discord_slash.utils.manage_components import (create_button,
create_actionrow)
from discord_slash.context import InteractionContext as IntCont
from PIL import ImageFont, Image, ImageDraw
from gwendolyn.exceptions import GameNotInDatabase
from gwendolyn.utils import encode_id
class GameBase():
"""The base class for the games."""
def __init__(self, bot, game_name: int, drawer):
"""Initialize the class."""
self.bot = bot
self.long_strings = self.bot.long_strings
self.resources = "gwendolyn/resources/games/"
self.game_name = game_name
self.draw = drawer(bot, self)
def _get_action_rows(self, buttons: list[tuple[str, list]]):
self.bot.log("Generation action rows")
button_objects = []
for label, data, style in buttons:
custom_id = encode_id([self.game_name] + data)
button_objects.append(create_button(
style=style,
label=label,
custom_id=custom_id
))
action_rows = []
for i in range(((len(button_objects)-1)//5)+1):
action_rows.append(create_actionrow(*button_objects[i*5:i*5+5]))
return action_rows
async def _send_image(self, channel: Messageable,
buttons: list[tuple[str, list]] = None, delete=True):
self.draw.draw(str(channel.id))
file_path = f"{self.resources}images/{self.game_name}{channel.id}.png"
old_image = await channel.send(
file=File(file_path), delete_after=120 if delete else None)
if buttons is not None and len(buttons) < 25:
await old_image.edit(components = self._get_action_rows(buttons))
return old_image
class DatabaseGame(GameBase):
"""The base class for the games."""
def __init__(self, bot, game_name, drawer):
"""Initialize the class."""
super().__init__(bot, game_name, drawer)
self.database = self.bot.database
self.old_images_path = f"{self.resources}old_images/{self.game_name}"
def access_document(self, channel: str, collection_name: str="games",
raise_missing_error: bool=True):
collection = self.bot.database[f"{self.game_name} {collection_name}"]
game = collection.find_one({"_id": channel})
if game is None and raise_missing_error:
raise GameNotInDatabase(self.game_name, channel)
return game
def _test_document(self, channel: str, collection_name: str="games"):
collection = self.bot.database[f"{self.game_name} {collection_name}"]
game = collection.find_one({"_id": channel})
return game is not None
def _update_document(self, channel: str, updater: dict,
collection_name: str="games"):
self.database[f"{self.game_name} {collection_name}"].update_one(
{"_id": channel},
updater,
upsert=True
)
def _delete_document(self, channel: str, collection_name: str="games"):
self.database[f"{self.game_name} {collection_name}"].delete_one(
{"_id": channel}
)
def _insert_document(self, data: dict, collection_name: str="games"):
self.database[f"{self.game_name} {collection_name}"].insert_one(data)
async def _delete_old_image(self, channel: Messageable):
with open(self.old_images_path + str(channel.id), "r") as file_pointer:
old_image = await channel.fetch_message(int(file_pointer.read()))
await old_image.delete()
async def _send_image(self, channel: Messageable,
buttons: list[tuple[str, list]] = None, delete=True):
old_image = await super()._send_image(channel, buttons, delete)
with open(self.old_images_path + str(channel.id), "w") as file_pointer:
file_pointer.write(str(old_image.id))
async def _start_new(self, channel: Messageable, new_game: dict,
buttons: list[tuple[str, list]] = None,
delete = True):
new_game['_id'] = str(channel.id)
self._insert_document(new_game)
await self._send_image(channel, buttons, delete)
async def _end_game(self, channel: Messageable):
await self._delete_old_image(channel)
await self._send_image(channel, delete=False)
self._delete_document(str(channel.id))
class CardGame(DatabaseGame):
"""The class for card games."""
def __init__(self, bot, game_name, drawer, deck_used):
"""Initialize the class."""
super().__init__(bot, game_name, drawer)
self.decks_used = deck_used
def _shuffle_cards(self, channel: str):
self.bot.log(f"Shuffling cards for {self.game_name}")
with open(f"{self.resources}deck_of_cards.txt", "r") as file_pointer:
deck = file_pointer.read()
all_decks = deck.split("\n") * self.decks_used
random.shuffle(all_decks)
self._update_document(
channel,
{"$set": {"_id": channel, "cards": all_decks}},
"cards"
)
def _draw_card(self, channel: str):
"""
Draw a card from the stack.
*Parameters*
------------
channel: str
The id of the channel the card is drawn in.
"""
self.bot.log("drawing a card")
drawn_card = self.access_document(channel, "cards")["cards"][0]
self._update_document(channel, {"$pop": {"cards": -1}}, "cards")
return drawn_card
class BoardGame(GameBase):
async def _test_opponent(self, ctx: IntCont, opponent: Union[int, User]):
if isinstance(opponent, int):
# Opponent is Gwendolyn
if opponent in range(1, 6):
difficulty = int(opponent)
difficulty_text = f" with difficulty {difficulty}"
opponent = self.bot.user.id
else:
await ctx.send("Difficulty doesn't exist")
self.bot.log("They challenged a difficulty that doesn't exist")
return False
elif isinstance(opponent, User):
if opponent.bot:
# User has challenged a bot
if opponent == self.bot.user:
# It was Gwendolyn
difficulty = 3
difficulty_text = f" with difficulty {difficulty}"
opponent = self.bot.user.id
else:
await ctx.send("You can't challenge a bot!")
self.bot.log("They tried to challenge a bot")
return False
else:
# Opponent is another player
if ctx.author != opponent:
opponent = opponent.id
difficulty = 5
difficulty_text = ""
else:
await ctx.send("You can't play against yourself")
self.bot.log("They tried to play against themself")
return False
return opponent, (difficulty, difficulty_text)
class BaseDrawer():
"""Class for drawing games."""
def __init__(self, bot, game: GameBase):
self.bot = bot
self.game = game
self.fonts_path = "gwendolyn/resources/fonts/"
self.font_name = "futura-bold"
self.resources = game.resources
game_name = game.game_name
self.default_image = f"{self.resources}default_images/{game_name}.png"
self.default_size = None
self.default_color = None
self.images_path = f"{self.resources}images/{game_name}"
def _get_size(self, game: dict):
return self.default_size
def _draw_image(self, game: dict, image: Image.Image):
pass
def draw(self, channel: str):
game = self.game.access_document(channel)
if self.default_image is not None:
image = Image.open(self.default_image)
else:
image = Image.new("RGB", self._get_size(game), self.default_color)
self._draw_image(game, image)
self._save_image(image, channel)
def _get_font(self, size: int, font_name: str = None):
if font_name is None:
font_name = self.font_name
return ImageFont.truetype(f"{self.fonts_path}{font_name}.ttf", size)
def _adjust_font(self, max_font_size: int, text: str, max_text_size: int,
font_name: str = None):
font_size = max_font_size
font = self._get_font(font_size, font_name)
text_width = font.getsize(text)[0]
while text_width > max_text_size:
font_size -= 1
font = self._get_font(font_size, font_name)
text_width = font.getsize(text)[0]
return font, font_size
def _save_image(self, image: Image.Image, channel: str):
self.bot.log("Saving image")
image.save(f"{self.images_path}{channel}.png")
def _draw_shadow_text(self, text: str, colors: list[tuple[int, int, int]],
font_size: int):
font = self._get_font(font_size)
offset = font_size//20
shadow_offset = font_size//10
text_size = list(font.getsize(text))
text_size[0] += 1 + (offset * 2)
text_size[1] += 1 + (offset * 2) + shadow_offset
image = Image.new("RGBA", tuple(text_size), (0, 0, 0, 0))
text_image = ImageDraw.Draw(image)
for color_index, color in enumerate(colors[:-1]):
color_offset = offset//(color_index+1)
if color_index == 0:
color_shadow_offset = shadow_offset
else:
color_shadow_offset = 0
for i in range(4):
x_pos = [-1,1][i % 2] * color_offset
y_pos = [-1,1][(i//2) % 2] * color_offset + color_shadow_offset
position = (offset + x_pos, offset + y_pos)
text_image.text(position, text, fill=color, font=font)
text_image.text((offset, offset), text, fill=colors[-1], font=font)
return image
class CardDrawer(BaseDrawer):
def __init__(self, bot, game: GameBase):
super().__init__(bot, game)
self.cards_path = f"{self.resources}cards/"
# pylint: disable=invalid-name
self.CARD_WIDTH = 691
self.CARD_HEIGHT = 1065
self.CARD_BORDER = 100
self.CARD_OFFSET = 125
# pylint: enable=invalid-name
def _set_card_size(self, wanted_card_width: int):
ratio = wanted_card_width / self.CARD_WIDTH
self.CARD_WIDTH = wanted_card_width
self.CARD_HEIGHT = int(self.CARD_HEIGHT * ratio)
self.CARD_BORDER = int(self.CARD_BORDER * ratio)
self.CARD_OFFSET = int(self.CARD_OFFSET * ratio)
def _draw_cards(self, hand: list[str], hidden_cards: int = 0):
image_width = (self.CARD_BORDER * 2) + self.CARD_WIDTH
image_width += (self.CARD_OFFSET * (len(hand)-1))
image_size = (image_width, (self.CARD_BORDER * 2) + self.CARD_HEIGHT)
background = Image.new("RGBA", image_size, (0, 0, 0, 0))
for i, card in enumerate(hand):
position = (
self.CARD_BORDER + (self.CARD_OFFSET*i),
self.CARD_BORDER
)
if i + hidden_cards < len(hand):
card_image = Image.open(f"{self.cards_path}{card.upper()}.png")
else:
card_image = Image.open(f"{self.cards_path}red_back.png")
card_image = card_image.resize(
(self.CARD_WIDTH, self.CARD_HEIGHT),
resample=Image.BILINEAR
)
background.paste(card_image, position, card_image)
return background

View File

@@ -1,50 +0,0 @@
"""
Has a container for game functions.
*Classes*
---------
Games
Container for game functions.
"""
from .invest import Invest
from .trivia import Trivia
from .blackjack import Blackjack
from .connect_four import ConnectFour
from .hangman import Hangman
from .hex import HexGame
from .wordle import WordleGame
class Games():
"""
Contains game classes.
*Attributes*
------------
bot: Gwendolyn
The instance of Gwendolyn.
invest
Contains investment functions.
blackjack
Contains blackjack functions.
connect_four
Contains connect four functions.
hangman
Contains hangman functions.
hex
Contains hex functions
"""
def __init__(self, bot):
"""Initialize the container."""
self.bot = bot
self.invest = Invest(bot)
self.trivia = Trivia(bot)
self.blackjack = Blackjack(bot)
self.connect_four = ConnectFour(bot)
self.hangman = Hangman(bot)
self.hex = HexGame(bot)
self.wordle = WordleGame(bot)

View File

@@ -1,657 +0,0 @@
"""
Deals with commands and logic for hangman games.
*Classes*
---------
Hangman()
Deals with the game logic of hangman.
DrawHangman()
Draws the image shown to the player.
"""
import os
import datetime # Used for generating the game id
import string # string.ascii_uppercase used
import math # Used by DrawHangman(), mainly for drawing circles
import random # Used to draw poorly
import requests # Used for getting the word in Hangman.start()
import discord # Used for discord.file and type hints
from discord_slash.utils.manage_components import (create_button,
create_actionrow)
from discord_slash.model import ButtonStyle
from discord_slash.context import SlashContext, ComponentContext
# Used for typehints
from PIL import ImageDraw, Image, ImageFont # Used to draw the image
from gwendolyn.utils import encode_id
class Hangman():
"""
Controls hangman commands and game logic.
*Methods*
---------
start(ctx: SlashContext)
stop(ctx: SlashContext)
guess(message: discord.message, user: str, guess: str)
"""
def __init__(self, bot):
"""
Initialize the class.
*Attributes*
------------
draw: DrawHangman
The DrawHangman used to draw the hangman image.
API_url: str
The url to get the words from.
APIPARAMS: dict
The parameters to pass to every api call.
"""
self.bot = bot
self._draw = DrawHangman(bot)
self._API_url = "https://api.wordnik.com/v4/words.json/randomWords?" # pylint: disable=invalid-name
api_key = self.bot.credentials["wordnik_key"]
self._APIPARAMS = { # pylint: disable=invalid-name
"hasDictionaryDef": True,
"minCorpusCount": 5000,
"maxCorpusCount": -1,
"minDictionaryCount": 1,
"maxDictionaryCount": -1,
"minLength": 3,
"maxLength": 11,
"limit": 1,
"api_key": api_key
}
async def start(self, ctx: SlashContext):
"""
Start a game of hangman.
*Parameters*
------------
ctx: SlashContext
The context of the command.
"""
await self.bot.defer(ctx)
word = "-"
while "-" in word or "." in word:
response = requests.get(self._API_url, params=self._APIPARAMS)
word = list(response.json()[0]["word"].upper())
self.bot.log("Found the word \""+"".join(word)+"\"")
guessed = [False] * len(word)
game_id = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
remaining_letters = list(string.ascii_uppercase)
self._draw.draw_image(game_id, 0, word, guessed, [])
send_message = f"{ctx.author.display_name} started a game of hangman."
self.bot.log("Game started")
await ctx.send(send_message)
boards_path = "gwendolyn/resources/games/hangman_boards/"
file_path = f"{boards_path}hangman_board{game_id}.png"
image_message = await ctx.channel.send(
file=discord.File(file_path)
)
blank_message_one = await ctx.channel.send("_ _")
blank_message_two = await ctx.channel.send("_ _")
buttons = []
for letter in remaining_letters:
custom_id = encode_id([
"hangman",
"guess",
str(ctx.author.id),
letter,
"".join(word),
"",
game_id,
str(image_message.id),
str(blank_message_one.id),
str(blank_message_two.id)
])
buttons.append(create_button(
style=ButtonStyle.blue,
label=letter,
custom_id=custom_id
))
custom_id = encode_id([
"hangman",
"end",
str(ctx.author.id),
game_id,
str(image_message.id),
str(blank_message_one.id),
str(blank_message_two.id)
])
buttons.append(create_button(
style=ButtonStyle.red,
label="End game",
custom_id=custom_id
))
action_row_lists = []
for i in range(((len(buttons)-1)//20)+1):
action_rows = []
available_buttons = buttons[(i*20):(min(len(buttons),i*20+20))]
for x in range(((len(available_buttons)-1)//4)+1):
row_buttons = available_buttons[
(x*4):(min(len(available_buttons),x*4+4))
]
action_rows.append(create_actionrow(*row_buttons))
action_row_lists.append(action_rows)
await blank_message_one.edit(components=action_row_lists[0])
await blank_message_two.edit(components=action_row_lists[1])
async def stop(self, ctx: ComponentContext, game_id: str,
*messages: list[str]):
"""
Stop the game of hangman.
*Parameters*
------------
ctx: SlashContext
The context of the command.
"""
for msg_id in messages:
msg = await ctx.channel.fetch_message(msg_id)
await msg.delete()
self.bot.log("Deleting old messages")
await ctx.channel.send("Hangman game stopped")
boards_path = "gwendolyn/resources/games/hangman_boards/"
file_path = f"{boards_path}hangman_board{game_id}.png"
os.remove(file_path)
async def guess(self, ctx: ComponentContext, guess: str, word: str,
guessed_letters: str, game_id: str, *messages: list[str]):
"""
Guess a letter.
*Parameters*
------------
message: discord.Message
The message that the reaction was placed on.
user: str
The id of the user.
guess: str
The guess.
"""
for msg_id in messages:
msg = await ctx.channel.fetch_message(msg_id)
await msg.delete()
misses = 0
guessed_letters += guess
guessed = [False for i in range(len(word))]
for guessed_letter in guessed_letters:
correct_guess = 0
for i, letter in enumerate(word):
if guessed_letter == letter:
correct_guess += 1
guessed[i] = True
if correct_guess == 0:
misses += 1
remaining_letters = list(string.ascii_uppercase)
for letter in guessed_letters:
remaining_letters.remove(letter)
if correct_guess == 1:
send_message = "Guessed {}. There was 1 {} in the word."
send_message = send_message.format(guess, guess)
else:
send_message = "Guessed {}. There were {} {}s in the word."
send_message = send_message.format(guess, correct_guess, guess)
self._draw.draw_image(game_id, misses, word, guessed, guessed_letters)
if misses == 6:
send_message += self.bot.long_strings["Hangman lost game"]
remaining_letters = []
elif all(guessed):
self.bot.money.addMoney(f"#{ctx.author_id}", 15)
send_message += self.bot.long_strings["Hangman guessed word"]
remaining_letters = []
if remaining_letters != []:
await ctx.channel.send(send_message)
boards_path = "gwendolyn/resources/games/hangman_boards/"
file_path = f"{boards_path}hangman_board{game_id}.png"
image_message = await ctx.channel.send(
file=discord.File(file_path)
)
blank_message_one = await ctx.channel.send("_ _")
blank_message_two = await ctx.channel.send("_ _")
buttons = []
for letter in list(string.ascii_uppercase):
custom_id = encode_id([
"hangman",
"guess",
str(ctx.author.id),
letter,
word,
guessed_letters,
game_id,
str(image_message.id),
str(blank_message_one.id),
str(blank_message_two.id)
])
buttons.append(create_button(
style=ButtonStyle.blue,
label=letter,
custom_id=custom_id,
disabled=(letter not in remaining_letters)
))
custom_id = encode_id([
"hangman",
"end",
str(ctx.author.id),
game_id,
str(image_message.id),
str(blank_message_one.id),
str(blank_message_two.id)
])
buttons.append(create_button(
style=ButtonStyle.red,
label="End game",
custom_id=custom_id
))
action_row_lists = []
for i in range(((len(buttons)-1)//20)+1):
action_rows = []
available_buttons = buttons[(i*20):(min(len(buttons),i*20+20))]
for x in range(((len(available_buttons)-1)//4)+1):
row_buttons = available_buttons[
(x*4):(min(len(available_buttons),x*4+4))
]
action_rows.append(create_actionrow(*row_buttons))
action_row_lists.append(action_rows)
await blank_message_one.edit(components=action_row_lists[0])
await blank_message_two.edit(components=action_row_lists[1])
else:
boards_path = "gwendolyn/resources/games/hangman_boards/"
file_path = f"{boards_path}hangman_board{game_id}.png"
await ctx.channel.send(send_message, file=discord.File(file_path))
os.remove(file_path)
class DrawHangman():
"""
Draws the image of the hangman game.
*Methods*
---------
draw_image(channel: str)
"""
def __init__(self, bot):
"""
Initialize the class.
*Attributes*
------------
CIRCLESIZE
LINEWIDTH
BODYSIZE
LIMBSIZE
ARMPOSITION
MANX, MANY
LETTERLINELENGTH
LETTERLINEDISTANCE
GALLOWX, GALLOWY
PHI
FONT
SMALLFONT
"""
self._bot = bot
# pylint: disable=invalid-name
self._CIRCLESIZE = 120
self._LINEWIDTH = 12
self._BODYSIZE = 210
self._LIMBSIZE = 60
self._ARMPOSITION = 60
self._MANX = (self._LIMBSIZE*2)
self._MANY = (self._CIRCLESIZE+self._BODYSIZE+self._LIMBSIZE)
MANPADDING = self._LINEWIDTH*4
self._MANX += MANPADDING
self._MANY += MANPADDING
self._LETTERLINELENGTH = 90
self._LETTERLINEDISTANCE = 30
self._GALLOWX, self._GALLOWY = 360, 600
self._PHI = 1-(1 / ((1 + 5 ** 0.5) / 2))
LETTERSIZE = 75 # Wrong guesses letter size
WORDSIZE = 70 # Correct guesses letter size
FONTPATH = "gwendolyn/resources/fonts/comic-sans-bold.ttf"
self._FONT = ImageFont.truetype(FONTPATH, LETTERSIZE)
self._SMALLFONT = ImageFont.truetype(FONTPATH, WORDSIZE)
# pylint: enable=invalid-name
def _deviate(self, pre_deviance: int, pre_deviance_accuracy: int,
position_change: float, maxmin: int,
max_acceleration: float):
random_deviance = random.uniform(-position_change, position_change)
deviance_accuracy = pre_deviance_accuracy + random_deviance
if deviance_accuracy > maxmin * max_acceleration:
deviance_accuracy = maxmin * max_acceleration
elif deviance_accuracy < -maxmin * max_acceleration:
deviance_accuracy = -maxmin * max_acceleration
deviance = pre_deviance + deviance_accuracy
if deviance > maxmin:
deviance = maxmin
elif deviance < -maxmin:
deviance = -maxmin
return deviance, deviance_accuracy
def _bad_circle(self):
circle_padding = (self._LINEWIDTH*3)
image_width = self._CIRCLESIZE+circle_padding
image_size = (image_width, image_width)
background = Image.new("RGBA", image_size, color=(0, 0, 0, 0))
drawer = ImageDraw.Draw(background, "RGBA")
middle = (self._CIRCLESIZE+(self._LINEWIDTH*3))/2
deviance_x = 0
deviance_y = 0
deviance_accuracy_x = 0
deviance_accuracy_y = 0
start = random.randint(-100, -80)
degrees_amount = 360 + random.randint(-10, 30)
for degree in range(degrees_amount):
deviance_x, deviance_accuracy_x = self._deviate(
deviance_x,
deviance_accuracy_x,
self._LINEWIDTH/100,
self._LINEWIDTH,
0.03
)
deviance_y, deviance_accuracy_y = self._deviate(
deviance_y,
deviance_accuracy_y,
self._LINEWIDTH/100,
self._LINEWIDTH,
0.03
)
radians = math.radians(degree+start)
circle_x = (math.cos(radians) * (self._CIRCLESIZE/2))
circle_y = (math.sin(radians) * (self._CIRCLESIZE/2))
position_x = middle + circle_x - (self._LINEWIDTH/2) + deviance_x
position_y = middle + circle_y - (self._LINEWIDTH/2) + deviance_y
circle_position = [
(position_x, position_y),
(position_x+self._LINEWIDTH, position_y+self._LINEWIDTH)
]
drawer.ellipse(circle_position, fill=(0, 0, 0, 255))
return background
def _bad_line(self, length: int, rotated: bool = False):
if rotated:
width, height = length+self._LINEWIDTH*3, self._LINEWIDTH*3
else:
width, height = self._LINEWIDTH*3, length+self._LINEWIDTH*3
background = Image.new("RGBA", (width, height), color=(0, 0, 0, 0))
drawer = ImageDraw.Draw(background, "RGBA")
possible_deviance = int(self._LINEWIDTH/3)
deviance_x = random.randint(-possible_deviance, possible_deviance)
deviance_y = 0
deviance_accuracy_x = 0
deviance_accuracy_y = 0
for pixel in range(length):
deviance_x, deviance_accuracy_x = self._deviate(
deviance_x,
deviance_accuracy_x,
self._LINEWIDTH/1000,
self._LINEWIDTH,
0.004
)
deviance_y, deviance_accuracy_y = self._deviate(
deviance_y,
deviance_accuracy_y,
self._LINEWIDTH/1000,
self._LINEWIDTH,
0.004
)
if rotated:
position_x = self._LINEWIDTH + pixel + deviance_x
position_y = self._LINEWIDTH + deviance_y
else:
position_x = self._LINEWIDTH + deviance_x
position_y = self._LINEWIDTH + pixel + deviance_y
circle_position = [
(position_x, position_y),
(position_x+self._LINEWIDTH, position_y+self._LINEWIDTH)
]
drawer.ellipse(circle_position, fill=(0, 0, 0, 255))
return background
def _draw_man(self, misses: int, seed: str):
random.seed(seed)
man_size = (self._MANX, self._MANY)
background = Image.new("RGBA", man_size, color=(0, 0, 0, 0))
if misses >= 1:
head = self._bad_circle()
paste_x = (self._MANX-(self._CIRCLESIZE+(self._LINEWIDTH*3)))//2
paste_position = (paste_x, 0)
background.paste(head, paste_position, head)
if misses >= 2:
body = self._bad_line(self._BODYSIZE)
paste_x = (self._MANX-(self._LINEWIDTH*3))//2
paste_position = (paste_x, self._CIRCLESIZE)
background.paste(body, paste_position, body)
if misses >= 3:
limbs = random.sample(["rl", "ll", "ra", "la"], min(misses-2, 4))
else:
limbs = []
random.seed(seed)
for limb in limbs:
limb_drawing = self._bad_line(self._LIMBSIZE, True)
x_position = (self._MANX-(self._LINEWIDTH*3))//2
if limb[1] == "a":
rotation = random.randint(-45, 45)
shift = math.sin(math.radians(rotation))
line_length = self._LIMBSIZE+(self._LINEWIDTH*3)
compensation = int(shift*line_length)
limb_drawing = limb_drawing.rotate(rotation, expand=1)
y_position = self._CIRCLESIZE + self._ARMPOSITION
if limb == "ra":
compensation = min(-compensation, 0)
else:
x_position -= self._LIMBSIZE
compensation = min(compensation, 0)
y_position += compensation
else:
rotation = random.randint(-15, 15)
y_position = self._CIRCLESIZE+self._BODYSIZE-self._LINEWIDTH
if limb == "rl":
limb_drawing = limb_drawing.rotate(rotation-45, expand=1)
else:
x_position += -limb_drawing.size[0]+self._LINEWIDTH*3
limb_drawing = limb_drawing.rotate(rotation+45, expand=1)
paste_position = (x_position, y_position)
background.paste(limb_drawing, paste_position, limb_drawing)
return background
def _bad_text(self, text: str, big: bool, color: tuple = (0, 0, 0, 255)):
if big:
font = self._FONT
else:
font = self._SMALLFONT
width, height = font.getsize(text)
img = Image.new("RGBA", (width, height), color=(0, 0, 0, 0))
drawer = ImageDraw.Draw(img, "RGBA")
drawer.text((0, 0), text, font=font, fill=color)
return img
def _draw_gallows(self):
gallow_size = (self._GALLOWX, self._GALLOWY)
background = Image.new("RGBA", gallow_size, color=(0, 0, 0, 0))
bottom_line = self._bad_line(int(self._GALLOWX * 0.75), True)
bottom_line_x = int(self._GALLOWX * 0.125)
bottom_line_y = self._GALLOWY-(self._LINEWIDTH*4)
paste_position = (bottom_line_x, bottom_line_y)
background.paste(bottom_line, paste_position, bottom_line)
line_two = self._bad_line(self._GALLOWY-self._LINEWIDTH*6)
line_two_x = int(self._GALLOWX*(0.75*self._PHI))
line_two_y = self._LINEWIDTH*2
paste_position = (line_two_x, line_two_y)
background.paste(line_two, paste_position, line_two)
top_line = self._bad_line(int(self._GALLOWY*0.30), True)
paste_x = int(self._GALLOWX*(0.75*self._PHI))-self._LINEWIDTH
paste_position = (paste_x, self._LINEWIDTH*3)
background.paste(top_line, paste_position, top_line)
last_line = self._bad_line(int(self._GALLOWY*0.125))
paste_x += int(self._GALLOWY*0.30)
background.paste(last_line, (paste_x, self._LINEWIDTH*3), last_line)
return background
def _draw_letter_lines(self, word: str, guessed: list, misses: int):
letter_width = self._LETTERLINELENGTH+self._LETTERLINEDISTANCE
image_width = letter_width*len(word)
image_size = (image_width, self._LETTERLINELENGTH+self._LINEWIDTH*3)
letter_lines = Image.new("RGBA", image_size, color=(0, 0, 0, 0))
for i, letter in enumerate(word):
line = self._bad_line(self._LETTERLINELENGTH, True)
paste_x = i*(self._LETTERLINELENGTH+self._LETTERLINEDISTANCE)
paste_position = (paste_x, self._LETTERLINELENGTH)
letter_lines.paste(line, paste_position, line)
if guessed[i]:
letter_drawing = self._bad_text(letter, True)
letter_width = self._FONT.getsize(letter)[0]
letter_x = i*(self._LETTERLINELENGTH+self._LETTERLINEDISTANCE)
letter_x -= (letter_width//2)
letter_x += (self._LETTERLINELENGTH//2)+(self._LINEWIDTH*2)
letter_lines.paste(
letter_drawing,
(letter_x, 0),
letter_drawing
)
elif misses == 6:
letter_drawing = self._bad_text(letter, True, (242, 66, 54))
letter_width = self._FONT.getsize(letter)[0]
letter_x = i*(self._LETTERLINELENGTH+self._LETTERLINEDISTANCE)
letter_x -= (letter_width//2)
letter_x += (self._LETTERLINELENGTH//2)+(self._LINEWIDTH*2)
letter_lines.paste(
letter_drawing,
(letter_x, 0),
letter_drawing
)
return letter_lines
def _shortest_dist(self, positions: list, new_position: tuple):
shortest_dist = math.inf
for i, j in positions:
x_distance = abs(i-new_position[0])
y_distance = abs(j-new_position[1])
dist = math.sqrt(x_distance**2+y_distance**2)
if shortest_dist > dist:
shortest_dist = dist
return shortest_dist
def _draw_misses(self, guesses: list, word: str):
background = Image.new("RGBA", (600, 400), color=(0, 0, 0, 0))
pos = []
for guess in guesses:
if guess not in word:
placed = False
while not placed:
letter = self._bad_text(guess, True)
w, h = self._FONT.getsize(guess)
x = random.randint(0, 600-w)
y = random.randint(0, 400-h)
if self._shortest_dist(pos, (x, y)) > 70:
pos.append((x, y))
background.paste(letter, (x, y), letter)
placed = True
return background
def draw_image(self, game_id: str, misses: int, word: str,
guessed: list[bool], guessed_letters: str):
"""
Draw a hangman Image.
*Parameters*
------------
channel: str
The id of the channel the game is in.
"""
self._bot.log("Drawing hangman image")
random.seed(game_id)
background = Image.open("gwendolyn/resources/games/default_images/hangman.png")
gallow = self._draw_gallows()
man = self._draw_man(misses, game_id)
random.seed(game_id)
letter_line_parameters = [word, guessed, misses]
letter_lines = self._draw_letter_lines(*letter_line_parameters)
random.seed(game_id)
misses = self._draw_misses(list(guessed_letters), word)
background.paste(gallow, (100, 100), gallow)
background.paste(man, (300, 210), man)
background.paste(letter_lines, (120, 840), letter_lines)
background.paste(misses, (600, 150), misses)
misses_text = self._bad_text("MISSES", False)
misses_text_width = misses_text.size[0]
background.paste(misses_text, (850-misses_text_width//2, 50), misses_text)
boards_path = "gwendolyn/resources/games/hangman_boards/"
image_path = f"{boards_path}hangman_board{game_id}.png"
background.save(image_path)

View File

@@ -1,642 +0,0 @@
import random
import copy
import math
import discord
from PIL import Image, ImageDraw, ImageFont
class HexGame():
def __init__(self, bot):
self.bot = bot
self.draw = DrawHex(bot)
self.BOARDWIDTH = 11
self.ALLPOSITIONS = [(i,j) for i in range(11) for j in range(11)]
self.ALLSET = set(self.ALLPOSITIONS)
self.EMPTYDIJKSTRA = {}
for position in self.ALLPOSITIONS:
self.EMPTYDIJKSTRA[position] = math.inf # an impossibly high number
self.HEXDIRECTIONS = [(0,1),(-1,1),(-1,0),(0,-1),(1,-1),(1,0)]
async def surrender(self, ctx):
channel = str(ctx.channel_id)
game = self.bot.database["hex games"].find_one({"_id":channel})
user = f"#{ctx.author.id}"
players = game["players"]
if user not in players:
await ctx.send("You can't surrender when you're not a player.")
else:
opponent = (players.index(user) + 1) % 2
opponent_name = self.bot.database_funcs.get_name(players[opponent])
self.bot.database["hex games"].update_one({"_id":channel},{"$set":{"winner":opponent + 1}})
await ctx.send(f"{ctx.author.display_name} surrendered")
with open(f"gwendolyn/resources/games/old_images/hex{channel}", "r") as f:
old_image = await ctx.channel.fetch_message(int(f.read()))
if old_image is not None:
await old_image.delete()
else:
self.bot.log("The old image was already deleted")
self.bot.log("Sending the image")
file_path = f"gwendolyn/resources/games/hex_boards/board{channel}.png"
old_image = await ctx.channel.send(file = discord.File(file_path))
with open(f"gwendolyn/resources/games/old_images/hex{channel}", "w") as f:
f.write(str(old_image.id))
self.bot.database["hex games"].delete_one({"_id":channel})
# Swap
async def swap(self, ctx):
channel = str(ctx.channel_id)
game = self.bot.database["hex games"].find_one({"_id":channel})
user = f"#{ctx.author.id}"
if game is None:
await ctx.send("You can't swap nothing")
elif user not in game["players"]:
await ctx.send("You're not in the game")
elif len(game["gameHistory"]) != 1: # Only after the first move
await ctx.send("You can only swap as the second player after the very first move.")
elif user != game["players"][game["turn"]-1]:
await ctx.send("You can only swap after your opponent has placed their piece")
else:
self.bot.database["hex games"].update_one({"_id":channel},
{"$set":{"players":game["players"][::-1]}}) # Swaps their player-number
# Swaps the color of the hexes on the board drawing:
self.draw.drawSwap(channel)
opponent = game["players"][::-1][game["turn"]-1]
gwendolyn_turn = (opponent == f"#{self.bot.user.id}")
opponent_name = self.bot.database_funcs.get_name(opponent)
await ctx.send(f"The color of the players were swapped. It is now {opponent_name}'s turn")
with open(f"gwendolyn/resources/games/old_images/hex{channel}", "r") as f:
old_image = await ctx.channel.fetch_message(int(f.read()))
if old_image is not None:
await old_image.delete()
else:
self.bot.log("The old image was already deleted")
self.bot.log("Sending the image")
file_path = f"gwendolyn/resources/games/hex_boards/board{channel}.png"
old_image = await ctx.channel.send(file = discord.File(file_path))
with open(f"gwendolyn/resources/games/old_images/hex{channel}", "w") as f:
f.write(str(old_image.id))
if gwendolyn_turn:
await self.hexAI(ctx)
# Starts the game
async def start(self, ctx, opponent):
await self.bot.defer(ctx)
user = f"#{ctx.author.id}"
channel = str(ctx.channel_id)
game = self.bot.database["hex games"].find_one({"_id":channel})
started_game = False
can_start = True
if game != None:
send_message = "There's already a hex game going on in this channel"
log_message = "There was already a game going on"
can_start = False
else:
if type(opponent) == int:
# Opponent is Gwendolyn
if opponent in range(1, 6):
opponent_name = "Gwendolyn"
difficulty = int(opponent)
difficulty_text = f" with difficulty {difficulty}"
opponent = f"#{self.bot.user.id}"
else:
send_message = "Difficulty doesn't exist"
log_message = "They tried to play against a difficulty that doesn't exist"
can_start = False
elif type(opponent) == discord.member.Member:
if opponent.bot:
# User has challenged a bot
if opponent == self.bot.user:
# It was Gwendolyn
opponent_name = "Gwendolyn"
difficulty = 2
difficulty_text = f" with difficulty {difficulty}"
opponent = f"#{self.bot.user.id}"
else:
send_message = "You can't challenge a bot!"
log_message = "They tried to challenge a bot"
can_start = False
else:
# Opponent is another player
if ctx.author != opponent:
opponent_name = opponent.display_name
opponent = f"#{opponent.id}"
difficulty = 5
difficulty_text = ""
else:
send_message = "You can't play against yourself"
log_message = "They tried to play against themself"
can_start = False
else:
can_start = False
log_message = f"Opponent was neither int or member. It was {type(opponent)}"
send_message = "Something went wrong"
if can_start:
# board is 11x11
board = [[0 for i in range(self.BOARDWIDTH)] for j in range(self.BOARDWIDTH)]
players = [user, opponent]
random.shuffle(players) # random starting player
gameHistory = []
new_game = {"_id":channel,"board":board, "winner":0,
"players":players, "turn":1, "difficulty":difficulty, "gameHistory":gameHistory}
self.bot.database["hex games"].insert_one(new_game)
# draw the board
self.draw.drawBoard(channel)
gwendolyn_turn = (players[0] == f"#{self.bot.user.id}")
started_game = True
turn_name = self.bot.database_funcs.get_name(players[0])
send_message = f"Started Hex game against {opponent_name}{difficulty_text}. It's {turn_name}'s turn"
log_message = "Game started"
await ctx.send(send_message)
self.bot.log(log_message)
if started_game:
file_path = f"gwendolyn/resources/games/hex_boards/board{ctx.channel_id}.png"
new_image = await ctx.channel.send(file = discord.File(file_path))
with open(f"gwendolyn/resources/games/old_images/hex{ctx.channel_id}", "w") as f:
f.write(str(new_image.id))
if gwendolyn_turn:
await self.hexAI(ctx)
# Places a piece at the given location and checks things afterwards
async def placeHex(self, ctx, position : str, user):
channel = str(ctx.channel_id)
game = self.bot.database["hex games"].find_one({"_id":channel})
placed_piece = False
if game == None:
send_message = "There's no game in this channel"
self.bot.log("There was no game going on")
elif not (position[0].isalpha() and position[1:].isnumeric() and len(position) in [2, 3]):
send_message = "The position must be a letter followed by a number."
self.bot.log(f"The position was not valid, {position}")
else:
players = game["players"]
if user not in players:
send_message = f"You can't place when you're not in the game. The game's players are: {self.bot.database_funcs.get_name(game['players'][0])} and {self.bot.database_funcs.get_name(game['players'][1])}."
self.bot.log("They aren't in the game")
elif players[game["turn"]-1] != user:
send_message = "It's not your turn"
self.bot.log("It wasn't their turn")
else:
player = game["turn"]
turn = game["turn"]
board = game["board"]
self.bot.log("Placing a piece on the board with placeHex()")
# Places on board
board = self.placeOnHexBoard(board,player,position)
if board is None:
self.bot.log("It was an invalid position")
send_message = ("That's an invalid position. You must place your piece on an empty field.")
else:
# If the move is valid:
self.bot.database["hex games"].update_one({"_id":channel},{"$set":{"board":board}})
turn = (turn % 2) + 1
self.bot.database["hex games"].update_one({"_id":channel},{"$set":{"turn":turn}})
# Checking for a win
self.bot.log("Checking for win")
winner = self.evaluateBoard(game["board"])[1]
if winner == 0: # Continue with the game.
game_won = False
send_message = self.bot.database_funcs.get_name(game["players"][player-1])+" placed at "+position.upper()+". It's now "+self.bot.database_funcs.get_name(game["players"][turn-1])+"'s turn."# The score is "+str(score)
else: # Congratulations!
game_won = True
self.bot.database["hex games"].update_one({"_id":channel},{"$set":{"winner":winner}})
send_message = self.bot.database_funcs.get_name(game["players"][player-1])+" placed at "+position.upper()+" and won!"
if game["players"][winner-1] != f"#{self.bot.user.id}":
win_amount = game["difficulty"]*10
send_message += " Adding "+str(win_amount)+" GwendoBucks to their account."
self.bot.database["hex games"].update_one({"_id":channel},
{"$push":{"gameHistory":(int(position[1])-1, ord(position[0])-97)}})
# Is it now Gwendolyn's turn?
gwendolyn_turn = False
if game["players"][turn-1] == f"#{self.bot.user.id}":
self.bot.log("It's Gwendolyn's turn")
gwendolyn_turn = True
placed_piece = True
if user == f"#{self.bot.user.id}":
await ctx.channel.send(send_message)
else:
await ctx.send(send_message)
if placed_piece:
# Update the board
self.draw.drawHexPlacement(channel,player, position)
with open(f"gwendolyn/resources/games/old_images/hex{channel}", "r") as f:
old_image = await ctx.channel.fetch_message(int(f.read()))
if old_image is not None:
await old_image.delete()
else:
self.bot.log("The old image was already deleted")
self.bot.log("Sending the image")
file_path = f"gwendolyn/resources/games/hex_boards/board{channel}.png"
old_image = await ctx.channel.send(file = discord.File(file_path))
if game_won:
self.bot.log("Dealing with the winning player")
game = self.bot.database["hex games"].find_one({"_id":channel})
winner = game["winner"]
if game["players"][winner-1] != f"#{self.bot.user.id}":
winnings = game["difficulty"]*10
self.bot.money.addMoney(game["players"][winner-1].lower(),winnings)
self.bot.database["hex games"].delete_one({"_id":channel})
else:
with open(f"gwendolyn/resources/games/old_images/hex{channel}", "w") as f:
f.write(str(old_image.id))
if gwendolyn_turn:
await self.hexAI(ctx)
# Returns a board where the placement has ocurred
def placeOnHexBoard(self, board,player,position):
# Translates the position
position = position.lower()
# Error handling
column = ord(position[0]) - 97 # ord() translates from letter to number
row = int(position[1:]) - 1
if column not in range(self.BOARDWIDTH) or row not in range(self.BOARDWIDTH):
self.bot.log("Position out of bounds")
return None
# Place at the position
if board[row][column] == 0:
board[row][column] = player
return board
else:
self.bot.log("Cannot place on existing piece")
return None
# After your move, you have the option to undo get your turn back #TimeTravel
async def undo(self, ctx):
channel = str(ctx.channel_id)
user = f"#{ctx.author.id}"
undid = False
game = self.bot.database["hex games"].find_one({"_id":channel})
if user not in game["players"]:
send_message = "You're not a player in the game"
elif len(game["gameHistory"]) == 0:
send_message = "You can't undo nothing"
elif user != game["players"][(game["turn"] % 2)]: # If it's not your turn
send_message = "It's not your turn"
else:
turn = game["turn"]
self.bot.log("Undoing {}'s last move".format(self.bot.database_funcs.get_name(user)))
lastMove = game["gameHistory"].pop()
game["board"][lastMove[0]][lastMove[1]] = 0
self.bot.database["hex games"].update_one({"_id":channel},
{"$set":{"board":game["board"]}})
self.bot.database["hex games"].update_one({"_id":channel},
{"$set":{"turn":turn%2 + 1}})
# Update the board
self.draw.drawHexPlacement(channel,0,"abcdefghijk"[lastMove[1]]+str(lastMove[0]+1)) # The zero makes the hex disappear
send_message = f"You undid your last move at {lastMove}"
undid = True
await ctx.send(send_message)
if undid:
with open(f"gwendolyn/resources/games/old_images/hex{channel}", "r") as f:
old_image = await ctx.channel.fetch_message(int(f.read()))
if old_image is not None:
await old_image.delete()
else:
self.bot.log("The old image was already deleted")
self.bot.log("Sending the image")
file_path = f"gwendolyn/resources/games/hex_boards/board{channel}.png"
old_image = await ctx.channel.send(file = discord.File(file_path))
with open(f"gwendolyn/resources/games/old_images/hex{channel}", "w") as f:
f.write(str(old_image.id))
# Plays as the AI
async def hexAI(self, ctx):
channel = str(ctx.channel_id)
self.bot.log("Figuring out best move")
game = self.bot.database["hex games"].find_one({"_id":channel})
board = game["board"]
if len(game["gameHistory"]):
lastMove = game["gameHistory"][-1]
else:
lastMove = (5,5)
# These moves are the last move +- 2.
moves = [[(lastMove[0]+j-2,lastMove[1]+i-2) for i in range(5) if lastMove[1]+i-2 in range(11)] for j in range(5) if lastMove[0]+j-2 in range(11)]
moves = sum(moves,[])
movesCopy = moves.copy()
for move in movesCopy:
if board[move[0]][move[1]] != 0:
moves.remove(move)
chosenMove = random.choice(moves)
placement = "abcdefghijk"[chosenMove[1]]+str(chosenMove[0]+1)
self.bot.log(f"ChosenMove is {chosenMove} at {placement}")
await self.placeHex(ctx, placement, f"#{self.bot.user.id}")
def evaluateBoard(self, board):
scores = {1:0, 2:0}
winner = 0
# Here, I use Dijkstra's algorithm to evaluate the board, as proposed by this article: https://towardsdatascience.com/hex-creating-intelligent-adversaries-part-2-heuristics-dijkstras-algorithm-597e4dcacf93
for player in [1,2]:
Distance = copy.deepcopy(self.EMPTYDIJKSTRA)
# Initialize the starting hexes. For the blue player, this is the leftmost column. For the red player, this is the tom row.
for start in (self.ALLPOSITIONS[::11] if player == 2 else self.ALLPOSITIONS[:11]):
# An empty hex adds a of distance of 1. A hex of own color add distance 0. Opposite color adds infinite distance.
Distance[start] = 1 if (board[start[0]][start[1]] == 0) else 0 if (board[start[0]][start[1]] == player) else math.inf
visited = set() # Also called sptSet, short for "shortest path tree Set"
for _ in range(self.BOARDWIDTH**2): # We can at most check every 121 hexes
# Find the next un-visited hex, that has the lowest distance
remainingHexes = self.ALLSET.difference(visited)
A = [Distance[k] for k in remainingHexes] # Find the distance to each un-visited hex
u = list(remainingHexes)[A.index(min(A))] # Chooses the one with the lowest distance
# Find neighbors of the hex u
for di in self.HEXDIRECTIONS:
v = (u[0] + di[0] , u[1] + di[1]) # v is a neighbor of u
if v[0] in range(11) and v[1] in range(11) and v not in visited:
new_dist = Distance[u] + (1 if (board[v[0]][v[1]] == 0) else 0 if (board[v[0]][v[1]] == player) else math.inf)
Distance[v] = min(Distance[v], new_dist)
# After a hex has been visited, this is noted
visited.add(u)
#self.bot.log("Distance from player {}'s start to {} is {}".format(player,u,Distance[u]))
if u[player-1] == 10: # if the right coordinate of v is 10, it means we're at the goal
scores[player] = Distance[u] # A player's score is the shortest distance to goal. Which equals the number of remaining moves they need to win if unblocked by the opponent.
break
else:
self.bot.log("For some reason, no path to the goal was found. ")
if scores[player] == 0:
winner = player
break # We don't need to check the other player's score, if player1 won.
return scores[2]-scores[1], winner
def minimaxHex(self, board, depth, alpha, beta, maximizing_player):
# The depth is how many moves ahead the computer checks. This value is the difficulty.
if depth == 0 or 0 not in sum(board,[]):
score = self.evaluateBoard(board)[0]
return score
# if final depth is not reached, look another move ahead:
if maximizing_player: # red player predicts next move
maxEval = -math.inf
possiblePlaces = [i for i,v in enumerate(sum(board,[])) if v == 0]
#self.bot.log("Judging a red move at depth {}".format(depth))
for i in possiblePlaces:
test_board = copy.deepcopy(board)
test_board[i // self.BOARDWIDTH][i % self.BOARDWIDTH] = 1 # because maximizing_player is Red which is number 1
evaluation = self.minimaxHex(test_board,depth-1,alpha,beta,False)
maxEval = max(maxEval, evaluation)
alpha = max(alpha, evaluation)
if beta <= alpha:
#self.bot.log("Just pruned something!")
break
return maxEval
else: # blue player predicts next move
minEval = math.inf
possiblePlaces = [i for i,v in enumerate(sum(board,[])) if v == 0]
#self.bot.log("Judging a blue move at depth {}".format(depth))
for i in possiblePlaces:
test_board = copy.deepcopy(board)
test_board[i // self.BOARDWIDTH][i % self.BOARDWIDTH] = 2 # because minimizingPlayer is Blue which is number 2
evaluation = self.minimaxHex(test_board,depth-1,alpha,beta,True)
minEval = min(minEval, evaluation)
beta = min(beta, evaluation)
if beta <= alpha:
#self.bot.log("Just pruned something!")
break
return minEval
class DrawHex():
def __init__(self,bot):
self.bot = bot
# Defining all the variables
self.CANVASWIDTH = 2400
self.CANVASHEIGHT = 1800
self.SIDELENGTH = 75
# The offsets centers the board in the picture
self.XOFFSET = self.CANVASWIDTH/2 - 8*math.sqrt(3)*self.SIDELENGTH
# The offsets are the coordinates of the upperleft point in the
# upperleftmost hexagon
self.YOFFSET = self.CANVASHEIGHT/2 - 8*self.SIDELENGTH
# The whole width of one hexagon
self.HEXAGONWIDTH = math.sqrt(3) * self.SIDELENGTH
# The height difference between two layers
self.HEXAGONHEIGHT = 1.5 * self.SIDELENGTH
self.FONTSIZE = 45
self.TEXTCOLOR = (0,0,0)
self.FONT = ImageFont.truetype('gwendolyn/resources/fonts/futura-bold.ttf', self.FONTSIZE)
self.LINETHICKNESS = 15
self.HEXTHICKNESS = 6 # This is half the width of the background lining between every hex
self.XTHICKNESS = self.HEXTHICKNESS * math.cos(math.pi/6)
self.YTHICKNESS = self.HEXTHICKNESS * math.sin(math.pi/6)
self.BACKGROUNDCOLOR = (230,230,230)
self.BETWEENCOLOR = (231,231,231)
self.BLANKCOLOR = "lightgrey"
self.PIECECOLOR = {1:(237,41,57),2:(0,165,255),0:self.BLANKCOLOR} # player1 is red, player2 is blue
self.BOARDCOORDINATES = [ [(self.XOFFSET + self.HEXAGONWIDTH*(column + row/2),self.YOFFSET + self.HEXAGONHEIGHT*row) for column in range(11)] for row in range(11)] # These are the coordinates for the upperleft corner of every hex
self.COLHEXTHICKNESS = 4 # When placing a hex, it is a little bigger than the underlying hex in the background (4 < 6)
self.COLXTHICKNESS = self.COLHEXTHICKNESS * math.cos(math.pi/6)
self.COLYTHICKNESS = self.COLHEXTHICKNESS * math.sin(math.pi/6)
# The Name display things:
self.NAMESIZE = 60
self.NAMEFONT = ImageFont.truetype('gwendolyn/resources/fonts/futura-bold.ttf', self.NAMESIZE)
self.XNAME = {1:175, 2:self.CANVASWIDTH-100}
self.YNAME = {1:self.CANVASHEIGHT-150, 2:150}
self.NAMEHEXPADDING = 90
self.SMALLWIDTH = self.HEXAGONWIDTH * 0.6
self.SMALLSIDELENGTH = self.SIDELENGTH * 0.6
def drawBoard(self, channel):
self.bot.log("Drawing empty Hex board")
# Creates the empty image
im = Image.new('RGB', size=(self.CANVASWIDTH, self.CANVASHEIGHT),color = self.BACKGROUNDCOLOR)
# 'd' is a shortcut to drawing on the image
d = ImageDraw.Draw(im,"RGBA")
# Drawing all the hexagons
for column in self.BOARDCOORDINATES:
for startingPoint in column:
x = startingPoint[0]
y = startingPoint[1]
d.polygon([
(x, y),
(x+self.HEXAGONWIDTH/2, y-0.5*self.SIDELENGTH),
(x+self.HEXAGONWIDTH, y),
(x+self.HEXAGONWIDTH, y+self.SIDELENGTH),
(x+self.HEXAGONWIDTH/2, y+1.5*self.SIDELENGTH),
(x, y+self.SIDELENGTH),
],fill = self.BETWEENCOLOR)
d.polygon([
(x+self.XTHICKNESS, y + self.YTHICKNESS),
(x+self.HEXAGONWIDTH/2, y-0.5*self.SIDELENGTH + self.HEXTHICKNESS),
(x+self.HEXAGONWIDTH-self.XTHICKNESS, y + self.YTHICKNESS),
(x+self.HEXAGONWIDTH-self.XTHICKNESS, y+self.SIDELENGTH - self.YTHICKNESS),
(x+self.HEXAGONWIDTH/2, y+1.5*self.SIDELENGTH - self.HEXTHICKNESS),
(x+self.XTHICKNESS, y+self.SIDELENGTH - self.YTHICKNESS),
],fill = self.BLANKCOLOR)
# Draw color on the outside of the board
# Top line, red
d.line(sum((sum([(point[0],point[1],point[0]+self.HEXAGONWIDTH/2,point[1]-self.HEXAGONHEIGHT+self.SIDELENGTH) for point in self.BOARDCOORDINATES[0]],()),(self.BOARDCOORDINATES[0][10][0]+self.HEXAGONWIDTH*3/4,self.BOARDCOORDINATES[0][10][1]-self.SIDELENGTH/4)),()),
fill = self.PIECECOLOR[1],width = self.LINETHICKNESS)
# Bottom line, red
d.line(sum(((self.BOARDCOORDINATES[10][0][0]+self.HEXAGONWIDTH/4,self.BOARDCOORDINATES[10][0][1]+self.SIDELENGTH*5/4),sum([(point[0]+self.HEXAGONWIDTH/2,point[1]+self.HEXAGONHEIGHT,point[0]+self.HEXAGONWIDTH,point[1]+self.SIDELENGTH) for point in self.BOARDCOORDINATES[10]],())),()),
fill = self.PIECECOLOR[1],width = self.LINETHICKNESS)
# Left line, blue
d.line(sum((sum([(row[0][0],row[0][1],row[0][0],row[0][1]+self.SIDELENGTH) for row in self.BOARDCOORDINATES],()),(self.BOARDCOORDINATES[10][0][0]+self.HEXAGONWIDTH/4,self.BOARDCOORDINATES[10][0][1]+self.SIDELENGTH*5/4)),()),
fill = self.PIECECOLOR[2],width = self.LINETHICKNESS)
# Right line, blue
d.line(sum(((self.BOARDCOORDINATES[0][10][0]+self.HEXAGONWIDTH*3/4,self.BOARDCOORDINATES[0][10][1]-self.SIDELENGTH/4),sum([(row[10][0]+self.HEXAGONWIDTH,row[10][1],row[10][0]+self.HEXAGONWIDTH,row[10][1]+self.SIDELENGTH) for row in self.BOARDCOORDINATES],())),()),
fill = self.PIECECOLOR[2],width = self.LINETHICKNESS)
# Writes "abc..", "123.." on the columns and rows
for i in range(11):
# Top letters
d.text( (self.XOFFSET + self.HEXAGONWIDTH*i, self.YOFFSET-66) , "ABCDEFGHIJK"[i], font=self.FONT, fill=self.TEXTCOLOR)
# Bottom letters
d.text( (self.XOFFSET + self.HEXAGONWIDTH*(i+11.5/2), self.YOFFSET - 15 + 11*self.HEXAGONHEIGHT) , "ABCDEFGHIJK"[i], font=self.FONT, fill=self.TEXTCOLOR)
# Left numbers
d.multiline_text( (self.XOFFSET + self.HEXAGONWIDTH*i/2 - 72 -4*(i>8), self.YOFFSET + 18 + i*self.HEXAGONHEIGHT) , str(i+1), font=self.FONT, fill=self.TEXTCOLOR, align="right")
# Right numbers
d.text( (self.XOFFSET + self.HEXAGONWIDTH*(i/2+11) + 30 , self.YOFFSET + 6 + i*self.HEXAGONHEIGHT) , str(i+1), font=self.FONT, fill=self.TEXTCOLOR)
# Write player names and color
game = self.bot.database["hex games"].find_one({"_id":channel})
for p in [1,2]:
playername = self.bot.database_funcs.get_name(game["players"][p-1])
# Draw name
x = self.XNAME[p]
x -= self.NAMEFONT.getsize(playername)[0] if p==2 else 0 # player2's name is right-aligned
y = self.YNAME[p]
d.text((x,y),playername, font=self.NAMEFONT, fill = self.TEXTCOLOR)
# Draw a half-size Hexagon to indicate the player's color
x -= self.NAMEHEXPADDING # To the left of both names
d.polygon([
(x, y),
(x+self.SMALLWIDTH/2, y-self.SMALLSIDELENGTH/2),
(x+self.SMALLWIDTH, y),
(x+self.SMALLWIDTH, y+self.SMALLSIDELENGTH),
(x+self.SMALLWIDTH/2, y+self.SMALLSIDELENGTH*3/2),
(x, y+self.SMALLSIDELENGTH),
],fill = self.PIECECOLOR[p])
im.save("gwendolyn/resources/games/hex_boards/board"+channel+".png")
def drawHexPlacement(self, channel,player,position):
FILEPATH = "gwendolyn/resources/games/hex_boards/board"+channel+".png"
self.bot.log(f"Drawing a newly placed hex. Filename: board{channel}.png")
# Translates position
# We don't need to error-check, because the position is already checked in placeOnHexBoard()
position = position.lower()
column = ord(position[0])-97 # ord() translates from letter to number
row = int(position[1:])-1
# Find the coordinates for the filled hex drawing
hex_coords = [
(self.BOARDCOORDINATES[row][column][0]+self.COLXTHICKNESS, self.BOARDCOORDINATES[row][column][1] + self.COLYTHICKNESS),
(self.BOARDCOORDINATES[row][column][0]+self.HEXAGONWIDTH/2, self.BOARDCOORDINATES[row][column][1]-0.5*self.SIDELENGTH + self.COLHEXTHICKNESS),
(self.BOARDCOORDINATES[row][column][0]+self.HEXAGONWIDTH-self.COLXTHICKNESS, self.BOARDCOORDINATES[row][column][1] + self.COLYTHICKNESS),
(self.BOARDCOORDINATES[row][column][0]+self.HEXAGONWIDTH-self.COLXTHICKNESS, self.BOARDCOORDINATES[row][column][1]+self.SIDELENGTH - self.COLYTHICKNESS),
(self.BOARDCOORDINATES[row][column][0]+self.HEXAGONWIDTH/2, self.BOARDCOORDINATES[row][column][1]+1.5*self.SIDELENGTH - self.COLHEXTHICKNESS),
(self.BOARDCOORDINATES[row][column][0]+self.COLXTHICKNESS, self.BOARDCOORDINATES[row][column][1]+self.SIDELENGTH - self.COLYTHICKNESS),
]
# Opens the image
try:
with Image.open(FILEPATH) as im:
d = ImageDraw.Draw(im,"RGBA")
# Draws the hex piece
d.polygon(hex_coords,fill = self.PIECECOLOR[player], outline = self.BETWEENCOLOR)
# Save
im.save(FILEPATH)
except:
self.bot.log("Error drawing new hex on board")
def drawSwap(self, channel):
FILEPATH = "gwendolyn/resources/games/hex_boards/board"+channel+".png"
game = self.bot.database["hex games"].find_one({"_id":channel})
# Opens the image
try:
with Image.open(FILEPATH) as im:
d = ImageDraw.Draw(im,"RGBA")
# Write player names and color
for p in [1,2]:
playername = self.bot.database_funcs.get_name(game["players"][p%2])
x = self.XNAME[p]
x -= self.NAMEFONT.getsize(playername)[0] if p==2 else 0 # player2's name is right-aligned
y = self.YNAME[p]
# Draw a half-size Hexagon to indicate the player's color
x -= self.NAMEHEXPADDING # To the left of both names
d.polygon([
(x, y),
(x+self.SMALLWIDTH/2, y-self.SMALLSIDELENGTH/2),
(x+self.SMALLWIDTH, y),
(x+self.SMALLWIDTH, y+self.SMALLSIDELENGTH),
(x+self.SMALLWIDTH/2, y+self.SMALLSIDELENGTH*3/2),
(x, y+self.SMALLSIDELENGTH),
],fill = self.PIECECOLOR[p % 2 + 1])
# Save
im.save(FILEPATH)
except:
self.bot.log("Error drawing swap")

View File

@@ -1,286 +0,0 @@
"""
Contains functions relating to invest commands.
*Classes*
---------
Invest
Contains all the code for the invest commands.
"""
import discord # Used for embeds
from discord_slash.context import SlashContext # Used for type hints
class Invest():
"""
Contains all the invest functions.
*Methods*
---------
getPrice(symbol: str) -> int
getPortfolio(user: str) -> str
buyStock(user: str, stock: str: buyAmount: int) -> str
sellStock(user: str, stock: str, buyAmount: int) -> str
parseInvest(ctx: discord_slash.context.SlashContext,
parameters: str)
"""
def __init__(self, bot):
"""Initialize the class."""
self.bot = bot
def getPrice(self, symbol: str):
"""
Get the price of a stock.
*Parameters*
------------
symbol: str
The symbol of the stock to get the price of.
*Returns*
---------
price: int
The price of the stock.
"""
res = self.bot.finnhub_client.quote(symbol.upper())
if res == {}:
return 0
else:
return int(res["c"] * 100)
def getPortfolio(self, user: str):
"""
Get the stock portfolio of a user.
*Parameters*
------------
user: str
The id of the user to get the portfolio of.
*Returns*
---------
portfolio: str
The portfolio.
"""
investments_database = self.bot.database["investments"]
userInvestments = investments_database.find_one({"_id": user})
user_name = self.bot.database_funcs.get_name(user)
if userInvestments in [None, {}]:
return f"{user_name} does not have a stock portfolio."
else:
portfolio = f"**Stock portfolio for {user_name}**"
for key, value in list(userInvestments["investments"].items()):
purchaseValue = value["purchased for"]
stockPrice = self.getPrice(key)
valueAtPurchase = value["value at purchase"]
purchasedStock = value["purchased"]
valueChange = (stockPrice / valueAtPurchase)
currentValue = int(valueChange * purchasedStock)
portfolio += f"\n**{key}**: ___{currentValue} GwendoBucks___"
if purchaseValue != "?":
portfolio += f" (purchased for {purchaseValue})"
return portfolio
def buyStock(self, user: str, stock: str, buyAmount: int):
"""
Buy an amount of a specific stock.
*Paramaters*
------------
user: str
The id of the user buying.
stock: str
The symbol of the stock to buy.
buyAmount: int
The amount of GwendoBucks to use to buy the stock.
*Returns*
---------
send_message: str
The message to return to the user.
"""
if buyAmount < 100:
return "You cannot buy stocks for less than 100 GwendoBucks"
elif self.bot.money.checkBalance(user) < buyAmount:
return "You don't have enough money for that"
elif self.getPrice(stock) <= 0:
return f"{stock} is not traded on the american market."
else:
investments_database = self.bot.database["investments"]
stockPrice = self.getPrice(stock)
userInvestments = investments_database.find_one({"_id": user})
self.bot.money.addMoney(user, -1*buyAmount)
stock = stock.upper()
if userInvestments is not None:
userInvestments = userInvestments["investments"]
if stock in userInvestments:
value = userInvestments[stock]
valueChange = (stockPrice / value["value at purchase"])
currentValue = int(valueChange * value["purchased"])
newAmount = currentValue + buyAmount
value_path = f"investments.{stock}.value at purchase"
updater = {"$set": {value_path: stockPrice}}
investments_database.update_one({"_id": user}, updater)
purchased_path = f"investments.{stock}.purchased"
updater = {"$set": {purchased_path: newAmount}}
investments_database.update_one({"_id": user}, updater)
if value["purchased for"] != "?":
purchasedFor_path = f"investments.{stock}.purchased for"
updater = {"$set": {purchasedFor_path: buyAmount}}
investments_database.update_one({"_id": user}, updater)
else:
updater = {
"$set": {
"investments.{stock}": {
"purchased": buyAmount,
"value at purchase": stockPrice,
"purchased for": buyAmount
}
}
}
investments_database.update_one({"_id": user}, updater)
else:
new_user = {
"_id": user,
"investments": {
stock: {
"purchased": buyAmount,
"value at purchase": stockPrice,
"purchased for": buyAmount
}
}
}
investments_database.insert_one(new_user)
user_name = self.bot.database_funcs.get_name(user)
send_message = "{} bought {} GwendoBucks worth of {} stock"
send_message = send_message.format(user_name, buyAmount, stock)
return send_message
def sellStock(self, user: str, stock: str, sellAmount: int):
"""
Sell an amount of a specific stock.
*Paramaters*
------------
user: str
The id of the user selling.
stock: str
The symbol of the stock to sell.
buyAmount: int
The amount of GwendoBucks to sell for.
*Returns*
---------
send_message: str
The message to return to the user.
"""
if sellAmount <= 0:
return "no"
else:
investments_database = self.bot.database["investments"]
user_data = investments_database.find_one({"_id": user})
userInvestments = user_data["investments"]
stock = stock.upper()
if userInvestments is not None and stock in userInvestments:
value = userInvestments[stock]
stockPrice = self.getPrice(stock)
priceChange = (stockPrice / value["value at purchase"])
purchasedAmount = int(priceChange * value["purchased"])
purchased_path = f"investments.{stock}.purchased"
updater = {"$set": {purchased_path: purchasedAmount}}
investments_database.update_one({"_id": user}, updater)
valueAtPurchase_path = f"investments.{stock}.value at purchase"
updater = {"$set": {valueAtPurchase_path: stockPrice}}
investments_database.update_one({"_id": user}, updater)
if value["purchased"] >= sellAmount:
self.bot.money.addMoney(user, sellAmount)
if sellAmount < value["purchased"]:
purchased_path = f"investments.{stock}.purchased"
updater = {"$inc": {purchased_path: -sellAmount}}
investments_database.update_one({"_id": user}, updater)
purchasedFor_path = f"investments.{stock}.purchased for"
updater = {"$set": {purchasedFor_path: "?"}}
investments_database.update_one({"_id": user}, updater)
else:
updater = {"$unset": {f"investments.{stock}": ""}}
investments_database.update_one({"_id": user}, updater)
user_name = self.bot.database_funcs.get_name(user)
send_message = "{} sold {} GwendoBucks worth of {} stock"
return send_message.format(user_name, sellAmount, stock)
else:
return f"You don't have enough {stock} stocks to do that"
else:
return f"You don't have any {stock} stock"
async def parseInvest(self, ctx: SlashContext, parameters: str):
"""
Parse an invest command. TO BE DELETED.
*Parameters*
------------
ctx: discord_slash.context.SlashContext
The context of the slash command.
parameters: str
The parameters of the command.
"""
await self.bot.defer(ctx)
user = f"#{ctx.author.id}"
if parameters.startswith("check"):
commands = parameters.split(" ")
if len(commands) == 1:
response = self.getPortfolio(user)
else:
price = self.getPrice(commands[1])
if price == 0:
response = "{} is not traded on the american market."
response = response.format(commands[0].upper())
else:
price = f"{price:,}".replace(",", ".")
response = self.bot.long_strings["Stock value"]
response = response.format(commands[1].upper(), price)
elif parameters.startswith("buy"):
commands = parameters.split(" ")
if len(commands) == 3:
response = self.buyStock(user, commands[1], int(commands[2]))
else:
response = self.bot.long_strings["Stock parameters"]
elif parameters.startswith("sell"):
commands = parameters.split(" ")
if len(commands) == 3:
response = self.sellStock(user, commands[1], int(commands[2]))
else:
response = self.bot.long_strings["Stock parameters"]
else:
response = "Incorrect parameters"
if response.startswith("**"):
responses = response.split("\n")
text = "\n".join(responses[1:])
embedParams = {
"title": responses[0],
"description": text,
"colour": 0x00FF00
}
em = discord.Embed(**embedParams)
await ctx.send(embed=em)
else:
await ctx.send(response)

View File

@@ -3,149 +3,138 @@ Contains the code that deals with money.
*Classes* *Classes*
--------- ---------
Money Money
Deals with money. Deals with money.
""" """
import discord_slash # Used for typehints import typing
import discord # Used for typehints
import interactions # Used for typehints
if typing.TYPE_CHECKING:
from gwendolyn_client import Gwendolyn
class Money(): class Money():
"""
Deals with money.
*Methods*
---------
checkBalance(user: str)
sendBalance(ctx: interactions.SlashContext)
addMoney(user: str, amount: int)
giveMoney(ctx: interactions.SlashContext, user: discord.User,
amount: int)
*Attributes*
------------
bot: Gwendolyn
The instance of Gwendolyn
database: pymongo.Client
The mongo database
"""
def __init__(self, bot: "Gwendolyn"):
"""Initialize the class."""
self.bot = bot
self.database = bot.database
def checkBalance(self, user: str):
""" """
Deals with money. Get the account balance of a user.
*Methods* *Parameters*
---------
checkBalance(user: str)
sendBalance(ctx: discord_slash.context.SlashContext)
addMoney(user: str, amount: int)
giveMoney(ctx: discord_slash.context.SlashContext, user: discord.User,
amount: int)
*Attributes*
------------ ------------
bot: Gwendolyn user: str
The instance of Gwendolyn The user to get the balance of.
database: pymongo.Client
The mongo database *Returns*
---------
balance: int
The balance of the user's account.
""" """
self.bot.log("checking "+user+"'s account balance")
def __init__(self, bot): user_data = self.database["users"].find_one({"_id": user})
"""Initialize the class."""
self.bot = bot
self.database = bot.database
def checkBalance(self, user: str): if user_data is not None:
""" return user_data["money"]
Get the account balance of a user. else:
return 0
*Parameters* async def sendBalance(self, ctx: interactions.SlashContext):
------------ """
user: str Get your own account balance.
The user to get the balance of.
*Returns* *Parameters*
--------- ------------
balance: int ctx: interactions.SlashContext
The balance of the user's account. The context of the command.
""" """
self.bot.log("checking "+user+"'s account balance") await ctx.defer()
response = self.checkBalance("#"+str(ctx.author.id))
user_name = ctx.author.display_name
if response == 1:
new_message = f"{user_name} has 1 GwendoBuck"
else:
new_message = f"{user_name} has {response} GwendoBucks"
await ctx.send(new_message)
user_data = self.database["users"].find_one({"_id": user}) # Adds money to the account of a user
def addMoney(self, user: interactions.User, amount: int):
"""
Add money to a user account.
if user_data is not None: *Parameters*
return user_data["money"] ------------
else: user: User
return 0 The user to give money.
amount: int
The amount to add to the user's account.
"""
self.bot.log(f"adding {amount} to {user}'s account")
user_id = f"#{user.id}"
async def sendBalance(self, ctx: discord_slash.context.SlashContext): user_data = self.database["users"].find_one({"_id": user_id})
"""
Get your own account balance.
*Parameters* if user_data is not None:
------------ updater = {"$inc": {"money": amount}}
ctx: discord_slash.context.SlashContext self.database["users"].update_one({"_id": user_id}, updater)
The context of the command. else:
""" new_user = {
await self.bot.defer(ctx) "_id": user_id,
response = self.checkBalance("#"+str(ctx.author.id)) "user name": user.display_name,
user_name = ctx.author.display_name "money": amount
if response == 1: }
new_message = f"{user_name} has {response} GwendoBuck" self.database["users"].insert_one(new_user)
else:
new_message = f"{user_name} has {response} GwendoBucks"
await ctx.send(new_message)
# Adds money to the account of a user # Transfers money from one user to another
def addMoney(self, user: str, amount: int): async def giveMoney(self, ctx: interactions.SlashContext,
""" user: interactions.User, amount: int):
Add money to a user account. """
Give someone else money from your account.
*Parameters* *Parameters*
------------ ------------
user: str ctx: interactions.SlashContext
The id of the user to give money. The context of the command.
amount: int user: interactions.User
The amount to add to the user's account. The user to give money.
""" amount: int
self.bot.log("adding "+str(amount)+" to "+user+"'s account") The amount to transfer.
"""
await ctx.defer()
username = user.display_name
user_data = self.database["users"].find_one({"_id": user}) userid = f"#{ctx.author.id}"
user_data = self.database["users"].find_one({"_id": userid})
if user_data is not None: if amount <= 0:
updater = {"$inc": {"money": amount}} self.bot.log("They tried to steal")
self.database["users"].update_one({"_id": user}, updater) await ctx.send("Yeah, no. You can't do that")
else: elif user_data is None or user_data["money"] < amount:
new_user = { self.bot.log("They didn't have enough GwendoBucks")
"_id": user, await ctx.send("You don't have that many GwendoBuck")
"user name": self.bot.database_funcs.get_name(user), else:
"money": amount self.addMoney(ctx.author, -1 * amount)
} self.addMoney(user, amount)
self.database["users"].insert_one(new_user) await ctx.send(f"Transferred {amount} GwendoBucks to {username}")
# Transfers money from one user to another
async def giveMoney(self, ctx: discord_slash.context.SlashContext,
user: discord.User, amount: int):
"""
Give someone else money from your account.
*Parameters*
------------
ctx: discord_slash.context.SlashContext
The context of the command.
user: discord.User
The user to give money.
amount: int
The amount to transfer.
"""
await self.bot.defer(ctx)
username = user.display_name
if self.bot.database_funcs.get_id(username) is None:
async for member in ctx.guild.fetch_members(limit=None):
if member.display_name.lower() == username.lower():
username = member.display_name
user_id = f"#{member.id}"
new_user = {
"_id": user_id,
"user name": username,
"money": 0
}
self.bot.database["users"].insert_one(new_user)
userid = f"#{ctx.author.id}"
user_data = self.database["users"].find_one({"_id": userid})
targetUser = self.bot.database_funcs.get_id(username)
if amount <= 0:
self.bot.log("They tried to steal")
await ctx.send("Yeah, no. You can't do that")
elif targetUser is None:
self.bot.log("They weren't in the system")
await ctx.send("The target doesn't exist")
elif user_data is None or user_data["money"] < amount:
self.bot.log("They didn't have enough GwendoBucks")
await ctx.send("You don't have that many GwendoBuck")
else:
self.addMoney(f"#{ctx.author.id}", -1 * amount)
self.addMoney(targetUser, amount)
await ctx.send(f"Transferred {amount} GwendoBucks to {username}")

View File

@@ -1,205 +0,0 @@
"""
Contains code for trivia games.
*Classes*
---------
Trivia
Contains all the code for the trivia commands.
"""
import urllib # Used to get data from api
import json # Used to read data from api
import random # Used to shuffle answers
import asyncio # Used to sleep
from discord_slash.context import SlashContext # Used for type hints
class Trivia():
"""
Contains the code for trivia games.
*Methods*
---------
triviaStart(channel: str) -> str, str, str
triviaAnswer(user: str, channel: str, command: str) -> str
triviaCountPoints(channel: str)
triviaParse(ctx: SlashContext, answer: str)
"""
def __init__(self, bot):
"""Initialize the class."""
self.bot = bot
def triviaStart(self, channel: str):
"""
Start a game of trivia.
Downloads a question with answers, shuffles the wrong answers
with the correct answer and returns the questions and answers.
Also saves the question in the database.
*Parameters*
------------
channel: str
The id of the channel to start the game in
*Returns*
---------
send_message: str
The message to return to the user.
"""
triviaQuestions = self.bot.database["trivia questions"]
question = triviaQuestions.find_one({"_id": channel})
self.bot.log(f"Trying to find a trivia question for {channel}")
if question is None:
apiUrl = "https://opentdb.com/api.php?amount=10&type=multiple"
with urllib.request.urlopen(apiUrl) as response:
data = json.loads(response.read())
question = data["results"][0]["question"]
self.bot.log(f"Found the question \"{question}\"")
answers = data["results"][0]["incorrect_answers"]
answers.append(data["results"][0]["correct_answer"])
random.shuffle(answers)
correctAnswer = data["results"][0]["correct_answer"]
correctAnswer = answers.index(correctAnswer) + 97
newQuestion = {
"_id": channel,
"answer": str(chr(correctAnswer)),
"players": {}
}
triviaQuestions.insert_one(newQuestion)
replacements = {
"&#039;": "\'",
"&quot;": "\"",
"&ldquo;": "\"",
"&rdquo;": "\"",
"&eacute;": "é"
}
question = data["results"][0]["question"]
for key, value in replacements.items():
question = question.replace(key, value)
for answer in answers:
for key, value in replacements.items():
answer = answer.replace(key, value)
return question, answers, correctAnswer
else:
log_message = "There was already a trivia question for that channel"
self.bot.log(log_message)
return self.bot.long_strings["Trivia going on"], "", ""
def triviaAnswer(self, user: str, channel: str, command: str):
"""
Answer the current trivia question.
*Parameters*
------------
user: str
The id of the user who answered.
channel: str
The id of the channel the game is in.
command: str
The user's answer.
*Returns*
---------
send_message: str
The message to send if the function failed.
"""
triviaQuestions = self.bot.database["trivia questions"]
question = triviaQuestions.find_one({"_id": channel})
if command not in ["a", "b", "c", "d"]:
self.bot.log("I didn't quite understand that")
return "I didn't quite understand that"
elif question is None:
self.bot.log("There's no question right now")
return "There's no question right now"
elif user in question["players"]:
self.bot.log(f"{user} has already answered this question")
return f"{user} has already answered this question"
else:
self.bot.log(f"{user} answered the question in {channel}")
updater = {"$set": {f"players.{user}": command}}
triviaQuestions.update_one({"_id": channel}, updater)
return None
def triviaCountPoints(self, channel: str):
"""
Add money to every winner's account.
*Parameters*
------------
channel: str
The id of the channel the game is in.
"""
triviaQuestions = self.bot.database["trivia questions"]
question = triviaQuestions.find_one({"_id": channel})
self.bot.log("Counting points for question in "+channel)
if question is not None:
for player, answer in question["players"].items():
if answer == question["answer"]:
self.bot.money.addMoney(player, 1)
else:
self.bot.log("Couldn't find the questio")
return None
async def triviaParse(self, ctx: SlashContext, answer: str):
"""
Parse a trivia command.
*Parameters*
------------
ctx: SlashContext
The context of the command.
answer: str
The answer, if any.
"""
await self.bot.defer(ctx)
channelId = str(ctx.channel_id)
if answer == "":
question, options, correctAnswer = self.triviaStart(channelId)
if options != "":
results = "**"+question+"**\n"
for x, option in enumerate(options):
results += chr(x+97) + ") "+option+"\n"
await ctx.send(results)
await asyncio.sleep(60)
self.triviaCountPoints(channelId)
delete_gameParams = ["trivia questions", channelId]
self.bot.database_funcs.delete_game(*delete_gameParams)
self.bot.log("Time's up for the trivia question", channelId)
send_message = self.bot.long_strings["Trivia time up"]
format_parameters = [chr(correctAnswer), options[correctAnswer-97]]
send_message = send_message.format(*format_parameters)
await ctx.send(send_message)
else:
await ctx.send(question, hidden=True)
elif answer in ["a", "b", "c", "d"]:
userId = f"#{ctx.author.id}"
response = self.triviaAnswer(userId, channelId, answer)
if response is None:
user_name = ctx.author.display_name
await ctx.send(f"{user_name} answered **{answer}**")
else:
await ctx.send(response)
else:
self.bot.log("I didn't understand that", channelId)
await ctx.send("I didn't understand that")

View File

@@ -1,201 +0,0 @@
"""Implementation of Wordle"""
import requests
from PIL import Image, ImageDraw
from .game_base import DatabaseGame, BaseDrawer
from gwendolyn.exceptions import GameNotInDatabase
IMAGE_MARGIN = 90
SQUARE_SIZE = 150
SQUARE_PADDING = 25
ROW_PADDING = 30
FONT_SIZE = 130
# COLORS
BACKGROUND_COLOR = "#2c2f33"
TEXT_COLOR = "#eee"
WRITING_BORDER_COLOR = "#999"
INACTIVE_BORDER_COLOR = "#555"
INCORRECT_COLOR = "#444"
CORRECT_COLOR = "#0a8c20"
PRESENT_COLOR = "#0f7aa8"
COLORS = [INCORRECT_COLOR, PRESENT_COLOR, CORRECT_COLOR]
class WordleGame(DatabaseGame):
"""Implementation of wordle game."""
def __init__(self, bot):
super().__init__(bot, "wordle", WordleDrawer)
self._API_url = "https://api.wordnik.com/v4/words.json/randomWords?" # pylint: disable=invalid-name
api_key = self.bot.credentials["wordnik_key"]
self._APIPARAMS = { # pylint: disable=invalid-name
"hasDictionaryDef": True,
"minCorpusCount": 5000,
"maxCorpusCount": -1,
"minDictionaryCount": 1,
"maxDictionaryCount": -1,
"limit": 1,
"api_key": api_key
}
async def start(self, ctx, letters: int):
if self._test_document(str(ctx.channel_id)):
await ctx.send("There is already a wordle game in this channel.")
self.bot.log("There was already a game going on")
return
params = self._APIPARAMS
params["minLength"] = letters
params["maxLength"] = letters
word = "-"
while "-" in word or "." in word:
response = requests.get(self._API_url, params=params)
if response.json() == []:
ctx.send("Could not find a word. Try again")
return
word = list(response.json()[0]["word"].upper())
self.bot.log(f"Found the word \"{''.join(word)}\"")
game = {"word": word, "guesses": [], "results": []}
await ctx.send("Starting a wordle game.")
await self._start_new(ctx.channel, game, delete=False)
def _get_result(self, guess: list[str], word: list[str]):
result = ["_" for _ in guess]
for i, letter in enumerate(guess):
if letter == word[i]:
result[i] = "*"
for i, letter in enumerate(guess):
if letter in word and result[i] != "*":
same_letter = guess[:i].count(letter)
same_letter += len(
[
l for j,l in
enumerate(guess[i:])
if guess[i:][j] == letter and result[i:][j] == "*"
]
)
if same_letter < word.count(letter):
result[i] = "-"
return result
async def guess(self, ctx, guess: str):
if not guess.isalpha():
await ctx.send("You can only use letters in your guess")
return
guess = list(guess.upper())
try:
game = self.access_document(str(ctx.channel_id))
except GameNotInDatabase:
await ctx.send("No game in channel")
return
if len(guess) != len(game['word']):
await ctx.send(
f"Your guess must be {len(game['word'])} letters long")
return
await ctx.send(f"Guessed {''.join(guess)}")
result = self._get_result(guess, game['word'])
updater = {
"$set": {
f"guesses.{len(game['guesses'])}": guess,
f"results.{len(game['guesses'])}": result
}
}
self._update_document(str(ctx.channel_id), updater)
if result == ["*" for _ in game['word']]:
await ctx.send("You guessed the word! Adding 15 GwendoBucks to your account")
self.bot.money.addMoney(f"#{ctx.author_id}", 15)
await self._end_game(ctx.channel)
elif len(game['guesses']) == 5:
await ctx.send(f"You used up all available guesses. The word was '{''.join(game['word'])}'")
await self._end_game(ctx.channel)
else:
print(len(game['guesses']))
await self._delete_old_image(ctx.channel)
await self._send_image(ctx.channel, delete=False)
class WordleDrawer(BaseDrawer):
def __init__(self, bot, game: WordleGame):
super().__init__(bot, game)
self.default_image = None
self.default_color = BACKGROUND_COLOR
self.default_size = (0, 0)
def _get_size(self, game: dict):
width = (
(len(game['word']) * SQUARE_SIZE) +
((len(game['word']) - 1) * SQUARE_PADDING) +
(2 * IMAGE_MARGIN)
)
height = (
(6 * SQUARE_SIZE) +
(5 * ROW_PADDING) +
(2 * IMAGE_MARGIN)
)
size = (width, height)
return size
def _draw_row(self, row, drawer, font, word: str, colors: list[str] = None,
border_color: str = None):
if colors is None:
colors = [BACKGROUND_COLOR for _ in range(len(word))]
for i, letter in enumerate(word):
y_pos = IMAGE_MARGIN + (row * (SQUARE_SIZE + ROW_PADDING))
x_pos = IMAGE_MARGIN + (i * (SQUARE_SIZE + SQUARE_PADDING))
top_left = (x_pos, y_pos)
bottom_right = (x_pos + SQUARE_SIZE, y_pos + SQUARE_SIZE)
drawer.rounded_rectangle(
(top_left,bottom_right),
10,
colors[i],
border_color,
3
)
text_pos = (
x_pos + (SQUARE_SIZE//2),
y_pos + int(SQUARE_SIZE * 0.6)
)
drawer.text(text_pos, letter, TEXT_COLOR, font=font, anchor="mm")
def _determine_colors(self, results):
return [COLORS[["_","-","*"].index(symbol)] for symbol in results]
def _draw_image(self, game: dict, image: Image.Image):
drawer = ImageDraw.Draw(image)
font = self._get_font(FONT_SIZE)
for i, guess in enumerate(game['guesses']):
colors = self._determine_colors(game['results'][i])
self._draw_row(i, drawer, font, guess, colors)
if len(game["guesses"]) < 6:
self._draw_row(
len(game['guesses']),
drawer,
font,
" "*len(game['word']),
border_color=WRITING_BORDER_COLOR
)
for i in range(5 - len(game['guesses'])):
self._draw_row(
len(game['guesses']) + i + 1,
drawer,
font, " "*len(game['word']),
border_color=INACTIVE_BORDER_COLOR
)
return super()._draw_image(game, image)

View File

@@ -1,221 +0,0 @@
import math
import json
import discord
from gwendolyn.utils import cap
STATS = [
"strength",
"dexterity",
"constitution",
"intelligence",
"wisdom",
"charisma"
]
def mod(statistic):
"""Calculates D&D modifier."""
modifier = math.floor((statistic-10)/2)
if modifier >= 0:
modifier = "+"+str(modifier)
return modifier
class LookupFuncs():
def __init__(self, bot):
self.bot = bot
self.saves = [
"strength_save",
"dexterity_save",
"constitution_save",
"intelligence_save",
"wisdom_save",
"charisma_save"
]
self.abilities = [
"acrobatics",
"animal_handling",
"arcana",
"athletics",
"deception",
"history",
"insight",
"intimidation",
"investigation",
"medicine",
"nature",
"perception",
"performance",
"persuasion",
"religion",
"sleight_of_hand",
"stealth",
"survival"
]
def _format_monster(self, monster):
# Looks at the information about the monster and
# returns that information in separate variables,
# allowing Gwendolyn to know where to separate
# the messages
types = monster["type"]
if monster["subtype"] != "":
types += " ("+monster["subtype"]+")"
stats = []
for stat in STATS:
value = monster[stat]
stats.append(f"**{cap(stat[:3])}:** {value} ({mod(value)})")
stats = "\t".join(stats[:3]) + "\n" + "\t".join(stats[3:])
saving_throws = []
for save in self.saves:
if save in monster:
value = monster[save]
if monster[save] >= 0:
saving_throws.append(f"{cap(save[:3])} +{value}")
else:
saving_throws.append(f"{cap(save[:3])} {value}")
if saving_throws:
saving_throws = f"\n**Saving Throws:** {', '.join(saving_throws)}"
else:
saving_throws = ""
skills = []
for skill in self.abilities:
if skill in monster:
skill_name = cap(skill.replace("_"," "))
if monster[skill] >= 0:
skills.append(f"{skill_name} +{monster[skill]}")
else:
skills.append(f"{skill_name} {monster[skill]}")
if skills:
skills = f"\n**Skills:** {', '.join(skills)}"
else:
skills = ""
vulnerabilities = monster["damage_vulnerabilities"]
if vulnerabilities != "":
vulnerabilities = "\n**Damage Vulnerabilities** "+vulnerabilities
resistances = monster["damage_resistances"]
if resistances != "":
resistances = "\n**Damage Resistances** "+resistances
immunities = monster["damage_immunities"]
if immunities != "":
immunities = "\n**Damage Immunities** "+immunities
c_immunities = monster["condition_immunities"]
if c_immunities != "":
c_immunities = "\n**Condition Immunities** "+c_immunities
special_abilities = ""
if "special_abilities" in monster:
for ability in monster["special_abilities"]:
special_abilities += "\n\n***"+ability["name"]+".*** "+ability["desc"]
act = ""
if "actions" in monster:
for action in monster["actions"]:
act += "\n\n***"+action["name"]+".*** "+action["desc"]
react = ""
if "reactions" in monster:
for reaction in monster["reactions"]:
react += "\n\n***"+reaction["name"]+".*** "+reaction["desc"]
legendaryActions = ""
if "legendary_actions" in monster:
for action in monster["legendary_actions"]:
legendaryActions += "\n\n***"+action["name"]+".*** "+action["desc"]
hit_dice = monster["hit_dice"]
dice_amount = int(monster["hit_dice"].replace("d"," ").split()[0])
con_mod = math.floor((monster['constitution']-10)/2)
if con_mod < 0:
hit_dice += f" - {abs(con_mod) * dice_amount}"
elif con_mod > 0:
hit_dice += (f" + {con_mod * dice_amount}")
new_part = "\n--------------------"
monster_type = f"*{monster['size']} {types}, {monster['alignment']}*"
basic_info = "\n**Armor Class** "+str(monster["armor_class"])+"\n**Hit Points** "+str(monster["hit_points"])+" ("+hit_dice+")\n**Speed **"+monster["speed"]+new_part+"\n"
info = (monster_type+new_part+basic_info+stats+new_part+saving_throws+skills+vulnerabilities+resistances+immunities+c_immunities+"\n**Senses** "+monster["senses"]+"\n**Languages** "+monster["languages"]+"\n**Challenge** "+monster["challenge_rating"])
monster_info = [(info, monster['name']),
(special_abilities, "Special Abilities"),
(act, "Actions"),
(react, "Reactions"),
(legendaryActions, "Legendary Actions")]
self.bot.log("Returning monster information")
return monster_info
# Looks up a monster
async def monster_func(self, ctx, query):
query = cap(query)
self.bot.log("Looking up "+query)
# 1-letter monsters don't exist
if len(query) < 2:
self.bot.log("Monster name too short")
await ctx.send("I don't know that monster...")
return
# Opens "monsters.json"
monster_file_path = "gwendolyn/resources/lookup/monsters.json"
with open(monster_file_path,"r", encoding="utf-8") as file_pointer:
data = json.load(file_pointer)
for monster in data:
if "name" in monster and str(query) == monster["name"]:
self.bot.log("Found it!")
monster_info = self._format_monster(monster)
# Sends the received information. Separates into separate messages if
# there is too much text
await ctx.send(f"Result for \"{query}\"")
for text, title in monster_info:
if text != "":
if len(text) < 2000:
em = discord.Embed(title = title, description = text, colour=0xDEADBF)
await ctx.channel.send(embed = em)
else:
index = text[:2000].rfind(".")+1
em1 = discord.Embed(title = title, description = text[:index], colour=0xDEADBF)
await ctx.channel.send(embed = em1)
em2 = discord.Embed(title = "", description = text[index+1:], colour=0xDEADBF)
await ctx.channel.send(embed = em2)
break
else:
self.bot.log("Monster not in database")
await ctx.send("I don't know that monster...")
# Looks up a spell
async def spell_func(self, ctx, query):
query = cap(query)
self.bot.log("Looking up "+query)
# Opens "spells.json"
data = json.load(open('gwendolyn/resources/lookup/spells.json', encoding = "utf8"))
if query in data:
self.bot.log("Returning spell information")
send_message = (f"***{query}***\n*{data[query]['level']} level {data[query]['school']}\nCasting Time: {data[query]['casting_time']}\nRange:{data[query]['range']}\nComponents:{data[query]['components']}\nDuration:{data[query]['duration']}*\n \n{data[query]['description']}")
else:
self.bot.log("I don't know that spell")
send_message = "I don't think that's a spell"
if len(send_message) > 2000:
await ctx.send(send_message[:2000])
await ctx.send(send_message[2000:])
else:
await ctx.send(send_message)

View File

@@ -1,94 +0,0 @@
import random
class Generators():
def __init__(self, bot):
self.bot = bot
# Returns a list of all letter pairs in the text
def make_pairs(self, corpus):
for i in range(len(corpus)-1):
yield (corpus[i], corpus[i+1])
# Returns a list of all letter triplets in the text
def make_triplets(self, corpus):
for i in range(len(corpus)-2):
yield (corpus[i], corpus[i+1], corpus[i+2])
# Generates a random name
async def name_gen(self, ctx):
# Makes a list of all names from "names.txt"
with open("gwendolyn/resources/names.txt", "r", encoding='utf8') as file_pointer:
names = file_pointer.read()
corpus = list(names)
# Makes a list of pairs
pairs = self.make_pairs(corpus)
triplets = self.make_triplets(corpus)
letter_dict = {}
# Makes a dictionary of all letters that come after all other letters
for letter_1, letter_2 in pairs:
if letter_1 in letter_dict:
letter_dict[letter_1].append(letter_2)
else:
letter_dict[letter_1] = [letter_2]
for letter_1, letter_2, letter_3 in triplets:
if letter_1+letter_2 in letter_dict:
letter_dict[letter_1+letter_2].append(letter_3)
else:
letter_dict[letter_1+letter_2] = [letter_3]
# Choses a random first letter
first_letter = random.choice(corpus)
# Makes sure the first letter is not something a name can't start with.
while first_letter.islower() or first_letter == " " or first_letter == "-" or first_letter == "\n":
first_letter = random.choice(corpus)
# Starts the name
chain = [first_letter]
# Picks second letter
second_letter = random.choice(letter_dict[chain[-1]])
while second_letter == "\n":
second_letter = random.choice(letter_dict[chain[-1]])
chain.append(second_letter)
done = False
# Creates the name one letter at a time
while not done:
if random.randint(1,10) > 1:
try:
new_letter = random.choice(letter_dict[chain[-2]+chain[-1]])
except KeyError():
new_letter = random.choice(letter_dict[chain[-1]])
else:
new_letter = random.choice(letter_dict[chain[-1]])
chain.append(new_letter)
# Ends name if the name ends
if new_letter == "\n":
done = True
gen_name = "".join(chain)
self.bot.log("Generated "+gen_name[:-1])
# Returns the name
await ctx.send(gen_name)
# Generates a random tavern name
async def tavern_gen(self, ctx):
# _lists first parts, second parts and third parts of tavern names
first_part = ["The Silver","The Golden","The Staggering","The Laughing","The Prancing","The Gilded","The Running","The Howling","The Slaughtered","The Leering","The Drunken","The Leaping","The Roaring","The Frowning","The Lonely","The Wandering","The Mysterious","The Barking","The Black","The Gleaming","The Tap-Dancing","The Sad","The Sexy","The Artificial","The Groovy","The Merciful","The Confused","The Pouting","The Horny","The Okay","The Friendly","The Hungry","The Handicapped","The Fire-breathing","The One-Eyed","The Psychotic","The Mad","The Evil","The Idiotic","The Trusty","The Busty"]
second_part = ["Eel","Dolphin","Dwarf","Pegasus","Pony","Rose","Stag","Wolf","Lamb","Demon","Goat","Spirit","Horde","Jester","Mountain","Eagle","Satyr","Dog","Spider","Star","Dad","Rat","Jeremy","Mouse","Unicorn","Pearl","Ant","Crab","Penguin","Octopus","Lawyer","Ghost","Toad","Handjob","Immigrant","SJW","Dragon","Bard","Sphinx","Soldier","Salmon","Owlbear","Kite","Frost Giant","Arsonist"]
third_part = [" Tavern"," Inn","","","","","","","","",""]
# Picks one of each
gen_tav = random.choice(first_part)+" "+random.choice(second_part)+random.choice(third_part)
self.bot.log("Generated "+gen_tav)
# Return the name
await ctx.send(gen_tav)

View File

@@ -0,0 +1,44 @@
import random
class NameGenerator():
def __init__(self) -> None:
with open("gwendolyn/resources/names.txt", "r", encoding="UTF-8") as file_pointer:
self._names = file_pointer.read().lower().split("\n")
self._gen_letter_dict()
def _gen_letter_dict(self):
list_of_names = []
for name in self._names:
if (name != ""):
list_of_names.append("__" + name + "__")
dict_of_names = {}
for name in list_of_names:
for i in range(len(name)-3):
combination = name[i:i+2]
if combination not in dict_of_names:
dict_of_names[combination] = []
dict_of_names[combination].append(name[i+2])
self._letter_dict = dict_of_names
def generate(self):
combination = "__"
next_letter = ""
result = ""
while True:
number_of_letters = len(self._letter_dict[combination])
index = random.randint(0,number_of_letters - 1)
next_letter = self._letter_dict[combination][index]
if next_letter == "_":
break
else:
result = result + next_letter
combination = combination[1] + next_letter
return result.title()
if __name__ == "__main__":
print(NameGenerator().generate())

View File

@@ -1,85 +0,0 @@
import os
import requests
import discord
import wolframalpha
from PIL import Image, ImageDraw, ImageFont
class NerdShit():
def __init__(self, bot):
"""Runs misc commands."""
self.bot = bot
async def wolf_search(self,ctx,content):
await self.bot.defer(ctx)
font = ImageFont.truetype('gwendolyn/resources/fonts/times-new-roman.ttf', 20)
self.bot.log("Requesting data")
bot = wolframalpha.Client(self.bot.credentials["wolfram_alpha_key"])
res = bot.query(content)
self.bot.log("Processing data")
titles = []
pods = []
if int(res.numpods) > 0:
for pod in res.pods:
titles += [pod.title]
for i, sub in enumerate(pod.subpods):
pods += [sub]
if i > 0:
titles += [""]
pod_chunks = [pods[x:x+2] for x in range(0, len(pods), 2)]
title_chunks = [titles[x:x+2] for x in range(0, len(titles), 2)]
await ctx.send(f"Response for \"{content}\"")
for i, chunk in enumerate(pod_chunks):
width = 0
for title in title_chunks[i]:
width = max(width,font.getsize(title)[0])
height = 5
heights = []
for count, pod in enumerate(chunk):
heights += [height]
width = max(width,int(pod.img['@width']))
if title_chunks[i][count] == "":
place_for_text = 0
else:
place_for_text = 30
height += int(pod.img["@height"]) + 10 + place_for_text
width += 10
height += 5
wolf_image = Image.new("RGB",(width,height),color=(255,255,255))
for count, pod in enumerate(chunk):
response = requests.get(pod.img["@src"])
file = open("gwendolyn/resources/wolfTemp.png", "wb")
file.write(response.content)
file.close()
old_image = Image.open("gwendolyn/resources/wolfTemp.png")
old_size = old_image.size
if title_chunks[i][count] == "":
place_for_text = 0
else:
place_for_text = 30
new_size = (width,int(old_size[1]+10+place_for_text))
new_image = Image.new("RGB",new_size,color=(255,255,255))
new_image.paste(old_image, (int((int(old_size[0]+10)-old_size[0])/2),int(((new_size[1]-place_for_text)-old_size[1])/2)+place_for_text))
if title_chunks[i][count] != "":
drawer = ImageDraw.Draw(new_image,"RGB")
drawer.text((5,7),title_chunks[i][count],font=font,fill=(150,150,150))
wolf_image.paste(new_image,(0,heights[count]))
new_image.close()
old_image.close()
count += 1
wolf_image.save("gwendolyn/resources/wolf.png")
wolf_image.close()
await ctx.channel.send(file = discord.File("gwendolyn/resources/wolf.png"))
os.remove("gwendolyn/resources/wolf.png")
os.remove("gwendolyn/resources/wolfTemp.png")
else:
self.bot.log("No returned data")
await ctx.send("Could not find anything relating to your search")

View File

@@ -1,192 +1,65 @@
import random # Used in movie_func
import datetime # Used in hello_func import datetime # Used in hello_func
import urllib # Used in image_func
import ast
import imdb # Used in movie_func from interactions import SlashContext, Embed, SlashCommand
import discord # Used in movie_func
import lxml # Used in image_func
import fandom # Used in find_wiki_page
import d20 # Used in roll_dice
from .plex import Plex from .name_generator import NameGenerator
from .nerd_shit import NerdShit
from .generators import Generators
fandom.set_lang("da") from .roll import DieRoller
fandom.set_wiki("senkulpa")
class MyStringifier(d20.MarkdownStringifier): def gen_help_text(commands: list[SlashCommand]):
def _str_expression(self, node): return '\n'.join(sorted([f"`/{i.name}`\t{i.description}" for i in commands]))
if node.comment is None:
result_text = "Result"
else:
result_text = node.comment.capitalize()
return f"**{result_text}**: {self._stringify(node.roll)}\n**Total**: {int(node.total)}"
class Other(): class Other():
def __init__(self, bot): def __init__(self, bot):
self.bot = bot self.bot = bot
self.plex = Plex(self.bot) self._name_generator = NameGenerator()
self.nerd_shit = NerdShit(self.bot) self._roller = DieRoller()
self.generators = Generators(self.bot)
# Picks a random movie and returns information about it # Responds with a greeting of a time-appropriate maner
async def movie_func(self, ctx): async def hello_func(self, ctx: SlashContext):
await self.bot.defer(ctx) def time_in_range(start, end, i):
# Return true if i is in the range [start, end]
if start <= end:
return start <= i <= end
else:
return start <= i or i <= end
self.bot.log("Creating IMDb object") author = ctx.author.display_name
imdb_client = imdb.IMDb() now = datetime.datetime.now()
if time_in_range(now.replace(hour=5, minute=0, second=0, microsecond=0),now.replace(hour=10, minute=0, second=0, microsecond=0), now):
send_message = "Good morning, "+str(author)
elif time_in_range(now.replace(hour=13, minute=0, second=0, microsecond=0),now.replace(hour=18, minute=0, second=0, microsecond=0), now):
send_message = "Good afternoon, "+str(author)
elif time_in_range(now.replace(hour=18, minute=0, second=0, microsecond=0),now.replace(hour=22, minute=0, second=0, microsecond=0), now):
send_message = "Good evening, "+str(author)
elif time_in_range(now.replace(hour=22, minute=0, second=0, microsecond=0),now.replace(hour=23, minute=59, second=59, microsecond=0), now):
send_message = "Good night, "+str(author)
else:
send_message = "Hello, "+str(author)
self.bot.log("Picking a movie") await ctx.send(send_message)
with open("gwendolyn/resources/movies.txt", "r") as file_pointer:
movie_list = file_pointer.read().split("\n")
movie_name = random.choice(movie_list)
self.bot.log(f"Searching for {movie_name}") async def help_func(self, ctx: SlashContext, command: str):
search_result = imdb_client.search_movie(movie_name) if command == "":
text = gen_help_text([i for i in self.bot.application_commands if isinstance(i,SlashCommand)])
self.bot.log("Getting the data") embed = Embed(title = "Help", description = text,color = 0x59f442)
movie = search_result[0] await ctx.send(embed = embed)
imdb_client.update(movie) else:
self.bot.log(f"Looking for help-{command}.txt",str(ctx.channel_id))
self.bot.log("Successfully ran /movie") try:
with open(f"gwendolyn/resources/help/help-{command}.txt",encoding="utf-8") as file_pointer:
title = movie["title"] text = file_pointer.read()
plot = movie['plot'][0].split("::")[0] embed = Embed(title = command.capitalize(), description = text,color = 0x59f442)
cover = movie['cover url'].replace("150","600").replace("101","404")
cast = ", ".join([i["name"] for i in movie['cast'][:5]])
embed = discord.Embed(title=title, description=plot, color=0x24ec19)
embed.set_thumbnail(url=cover)
embed.add_field(name="Cast", value=cast,inline = True)
await ctx.send(embed = embed) await ctx.send(embed = embed)
except:
await ctx.send(f"Could not find a help file for the command '/{command}'")
# Responds with a greeting of a time-appropriate maner async def generate_name(self, ctx: SlashContext):
async def hello_func(self, ctx): await ctx.send(self._name_generator.generate())
def time_in_range(start, end, i):
# Return true if i is in the range [start, end]
if start <= end:
return start <= i <= end
else:
return start <= i or i <= end
author = ctx.author.display_name
now = datetime.datetime.now()
if time_in_range(now.replace(hour=5, minute=0, second=0, microsecond=0),now.replace(hour=10, minute=0, second=0, microsecond=0), now):
send_message = "Good morning, "+str(author)
elif time_in_range(now.replace(hour=13, minute=0, second=0, microsecond=0),now.replace(hour=18, minute=0, second=0, microsecond=0), now):
send_message = "Good afternoon, "+str(author)
elif time_in_range(now.replace(hour=18, minute=0, second=0, microsecond=0),now.replace(hour=22, minute=0, second=0, microsecond=0), now):
send_message = "Good evening, "+str(author)
elif time_in_range(now.replace(hour=22, minute=0, second=0, microsecond=0),now.replace(hour=23, minute=59, second=59, microsecond=0), now):
send_message = "Good night, "+str(author)
else:
send_message = "Hello, "+str(author)
await ctx.send(send_message)
# Finds a random picture online
async def image_func(self, ctx):
# Picks a type of camera, which decides the naming scheme
cams = ("one","two","three","four")
cam = random.choice(cams)
self.bot.log("Chose cam type "+cam)
if cam == "one":
search = "img_" + ''.join(
[str(random.randint(0,9)) for _ in range(4)]
)
elif cam == "two":
year = str(random.randint(2012,2016))
month = str(random.randint(1,12)).zfill(2)
day = str(random.randint(1,29)).zfill(2)
search = f"IMG_{year}{month}{day}"
elif cam == "three":
search = f"IMAG_{str(random.randint(1,500)).zfill(4)}"
elif cam == "four":
search = "DSC_" + ''.join(
[str(random.randint(0,9)) for _ in range(4)]
)
self.bot.log("Searching for "+search)
# Searches for the image and reads the resulting web page
page = urllib.request.urlopen("https://www.bing.com/images/search?q="+search+"&safesearch=off")
read = page.read()
tree = lxml.etree.HTML(read)
images = tree.xpath('//a[@class = "iusc"]/@m')
if len(images) == 0:
await ctx.send("Found no images")
else:
# Picks an image
number = random.randint(1,len(images))-1
image = ast.literal_eval(str(images[number]))
image_url = image["murl"]
self.bot.log("Picked image number "+str(number))
# Returns the image
self.bot.log("Successfully returned an image")
await ctx.send(image_url)
# Finds a page from the Senkulpa Wikia
async def find_wiki_page(self, ctx, search : str):
await self.bot.defer(ctx)
found_page = False
if search != "":
self.bot.log("Trying to find wiki page for "+search)
search_results = fandom.search(search)
if len(search_results) > 0:
found_page = True
search_result = search_results[0]
else:
self.bot.log("Couldn't find the page")
await ctx.send("Couldn't find page")
else:
found_page = True
self.bot.log("Searching for a random page")
search_result = fandom.random()
if found_page:
self.bot.log(f"Found page \"{search_result[0]}\"")
page = fandom.page(pageid = search_result[1])
content = page.summary
images = page.images
if len(images) > 0:
image = images[0]
else:
image = ""
self.bot.log("Sending the embedded message",str(ctx.channel_id))
content += f"\n[Læs mere]({page.url})"
embed = discord.Embed(title = page.title, description = content, colour=0xDEADBF)
if image != "":
embed.set_thumbnail(url=image)
await ctx.send(embed = embed)
async def roll_dice(self, ctx, roll_string):
user = ctx.author.display_name
while len(roll_string) > 1 and roll_string[0] == " ":
roll_string = roll_string[1:]
roll = d20.roll(roll_string, allow_comments=True, stringifier=MyStringifier())
await ctx.send(f"{user} :game_die:\n{roll}")
async def help_func(self, ctx, command):
if command == "":
with open("gwendolyn/resources/help/help.txt",encoding="utf-8") as file_pointer:
text = file_pointer.read()
embed = discord.Embed(title = "Help", description = text,colour = 0x59f442)
await ctx.send(embed = embed)
else:
self.bot.log(f"Looking for help-{command}.txt",str(ctx.channel_id))
with open(f"gwendolyn/resources/help/help-{command}.txt",encoding="utf-8") as file_pointer:
text = file_pointer.read()
embed = discord.Embed(title = command.capitalize(), description = text,colour = 0x59f442)
await ctx.send(embed = embed)
async def roll_dice(self, ctx: SlashContext, dice: str):
try:
roll = self._roller.roll(dice)
await ctx.send(f":game_die:Rolling dice `{dice}`\n{roll}")
except:
await ctx.send("There was an error in the rolling")

View File

@@ -1,600 +0,0 @@
"""Plex integration with the bot."""
from math import floor, ceil
import time
import asyncio
import requests
import imdb
import discord
import xmltodict
from discord_slash.utils.manage_components import (create_button,
create_actionrow)
from discord_slash.model import ButtonStyle
from gwendolyn.utils import encode_id
class Plex():
"""Container for Plex functions and commands."""
def __init__(self,bot):
self.bot = bot
self.credentials = self.bot.credentials
self.long_strings = self.bot.long_strings
server_ip = ["localhost", "192.168.0.40"][self.bot.options["testing"]]
self.radarr_url = "http://"+server_ip+":7878/api/v3/"
self.sonarr_url = "http://"+server_ip+":8989/api/"
self.qbittorrent_url = "http://"+server_ip+":8080/api/v2/"
self.movie_path = "/media/plex/Server/movies/"
self.show_path = "/media/plex/Server/Shows/"
async def request_movie(self, ctx, movie_name):
"""Request a movie for the Plex Server"""
await self.bot.defer(ctx)
self.bot.log("Searching for "+movie_name)
movie_list = imdb.IMDb().search_movie(movie_name)
movies = []
for movie in movie_list:
if movie["kind"] == "movie":
movies.append(movie)
if len(movies) > 5:
movies = movies[:5]
if len(movies) == 1:
message_title = "**Is it this movie?**"
else:
message_title = "**Is it any of these movies?**"
message_text = ""
imdb_ids = []
for i, movie in enumerate(movies):
try:
message_text += "\n"+str(i+1)+") "+movie["title"]
try:
message_text += " ("+str(movie["year"])+")"
except KeyError:
self.bot.log(f"{movie['title']} has no year.")
except KeyError:
message_text += "Error"
imdb_ids.append(movie.movieID)
self.bot.log(
f"Returning a list of {len(movies)} possible movies: {imdb_ids}"
)
embed = discord.Embed(
title=message_title,
description=message_text,
colour=0x00FF00
)
buttons = []
if len(movies) == 1:
buttons.append(create_button(
style=ButtonStyle.green,
label="",
custom_id=encode_id(["plex", "movie", str(imdb_ids[0])])
)
)
else:
for i in range(len(movies)):
buttons.append(
create_button(
style=ButtonStyle.blue,
label=str(i+1),
custom_id=encode_id(["plex", "movie", str(imdb_ids[i])])
)
)
buttons.append(create_button(
style=ButtonStyle.red,
label="X",
custom_id=encode_id(["plex", "movie", "x"])
)
)
action_rows = []
for i in range(((len(buttons)-1)//5)+1):
action_rows.append(
create_actionrow(
*buttons[(i*5):(min(len(buttons),i*5+5))]
)
)
await ctx.send(embed=embed, components=action_rows)
async def add_movie(self, message, imdb_id, edit_message = True):
"""Add a movie to Plex server."""
if not edit_message:
await message.delete()
if imdb_id == "X":
self.bot.log("Did not find what the user was searching for")
message_text = "Try searching for the IMDB id"
else:
self.bot.log("Trying to add movie "+str(imdb_id))
# Searches for the movie using the imdb id through Radarr
api_key = self.credentials["radarr_key"]
request_url = self.radarr_url+"movie/lookup/imdb?imdbId=tt"+imdb_id
request_url += "&apiKey="+api_key
response = requests.get(request_url)
# Makes the dict used for the post request
lookup_data = response.json()
post_data = {
"qualityProfileId": 1,
"rootFolderPath" : self.movie_path,
"monitored" : True,
"addOptions": {"searchForMovie": True}
}
for key in ["tmdbId","title","titleSlug","images","year"]:
post_data.update({key : lookup_data[key]})
# Makes the post request
response = requests.post(
url = self.radarr_url+"movie?apikey="+api_key,
json = post_data
)
# Deciphers the response
if response.status_code == 201:
self.bot.log("Added "+post_data["title"]+" to Plex")
message_text = "{} successfully added to Plex".format(
post_data["title"]
)
elif response.status_code == 400:
self.bot.log("The movie was already on plex")
message_text = self.long_strings["Already on Plex"].format(
post_data['title']
)
else:
self.bot.log(str(response.status_code)+" "+response.reason)
message_text = "Something went wrong",
if edit_message:
await message.edit(
embed = None,
content = message_text,
components = []
)
else:
await message.channel.send(message_text)
async def request_show(self, ctx, show_name):
"""Request a show for the Plex server."""
await self.bot.defer(ctx)
self.bot.log("Searching for "+show_name)
movies = imdb.IMDb().search_movie(show_name) # Replace with tvdb
shows = []
for movie in movies:
if movie["kind"] in ["tv series","tv miniseries"]:
shows.append(movie)
if len(shows) > 5:
shows = shows[:5]
if len(shows) == 1:
message_title = "**Is it this show?**"
else:
message_title = "**Is it any of these shows?**"
message_text = ""
imdb_ids = []
for i, show in enumerate(shows):
try:
message_text += f"\n{i+1}) {show['title']} ({show['year']})"
except KeyError:
try:
message_text += "\n"+str(i+1)+") "+show["title"]
except KeyError:
message_text += "Error"
imdb_ids.append(show.movieID)
self.bot.log(
f"Returning a list of {len(shows)} possible shows: {imdb_ids}"
)
embed = discord.Embed(
title=message_title,
description=message_text,
colour=0x00FF00
)
buttons = []
if len(shows) == 1:
buttons.append(create_button(
style=ButtonStyle.green,
label="",
custom_id=encode_id(["plex", "show", str(imdb_ids[0])])
)
)
else:
for i in range(len(shows)):
buttons.append(
create_button(
style=ButtonStyle.blue,
label=str(i+1),
custom_id=encode_id(["plex", "show", str(imdb_ids[i])])
)
)
buttons.append(create_button(
style=ButtonStyle.red,
label="X",
custom_id=encode_id(["plex", "show", "x"])
)
)
action_rows = []
for i in range(((len(buttons)-1)//5)+1):
action_rows.append(
create_actionrow(
*buttons[(i*5):(min(len(buttons),i*5+5))]
)
)
await ctx.send(embed=embed, components=action_rows)
async def add_show(self, message, imdb_id, edit_message = True):
"""Add the requested show to Plex."""
if imdb_id == "X":
self.bot.log("Did not find what the user was searching for")
message_text = "Try searching for the IMDB id"
else:
self.bot.log("Trying to add show "+str(imdb_id))
# Finds the tvdb id
tvdb_api_url = "https://thetvdb.com/api/"
tvdb_method = "GetSeriesByRemoteID.php"
tvdb_request_url = f"{tvdb_api_url}{tvdb_method}"
tvdb_id = xmltodict.parse(
requests.get(
tvdb_request_url+f"?imdbid=tt{imdb_id}",
headers = {"ContentType" : "application/json"}
).text
)['Data']['Series']['seriesid']
# Finds the rest of the information using Sonarr
api_key = self.credentials["sonarr_key"]
request_url = self.sonarr_url+"series/lookup?term="
request_url += f"tvdb:{tvdb_id}"
request_url += "&apiKey="+api_key
response = requests.get(request_url)
# Makes the dict used for the post request
lookup_data = response.json()[0]
post_data = {
"ProfileId" : 1,
"rootFolder_path" : self.show_path,
"monitored" : True,
"addOptions" : {"searchForMissingEpisodes" : True}
}
for key in ["tvdbId","title","titleSlug","images","seasons"]:
post_data.update({key : lookup_data[key]})
# Makes the post request
response = requests.post(
url= self.sonarr_url+"series?apikey="+api_key,
json = post_data
)
# Deciphers the response
if response.status_code == 201:
self.bot.log("Added a "+post_data["title"]+" to Plex")
message_text = post_data["title"]+" successfully added to Plex"
elif response.status_code == 400:
message_text = self.long_strings["Already on Plex"].format(
post_data['title']
)
else:
self.bot.log(str(response.status_code)+" "+response.reason)
message_text = "Something went wrong"
if edit_message:
await message.edit(
embed = None,
content = message_text,
components = []
)
else:
await message.channel.send(message_text)
async def __generate_download_list(self, show_dm, show_movies, show_shows,
episodes):
"""Generate a list of all torrents.
*Returns*
message_text: str
A formatted list of all torrents
all_downloaded: bool
Whether all torrents are downloaded
"""
self.bot.log("Generating torrent list")
title_width = 100
message = []
all_downloaded = True
if show_dm:
message.append("")
dm_section_title = "*Torrent Downloads*"
dm_section_title_line = "-"*((title_width-len(dm_section_title))//2)
message.append(
dm_section_title_line+dm_section_title+dm_section_title_line
)
login_url = self.qbittorrent_url+"auth/login"
username = self.credentials["qbittorrent_username"]
password = self.credentials["qbittorrent_password"]
login_url += f"?username={username}&password={password}"
cookie = {"SID": requests.get(login_url).cookies.values()[0]}
response = requests.get(
self.qbittorrent_url+"torrents/info",
cookies=cookie
)
torrent_list = response.json()
if len(torrent_list) > 0:
for torrent in torrent_list:
if torrent['category'] not in ["radarr", "tv-sonarr"]:
break
torrent_name = torrent["name"]
if len(torrent_name) > 30:
if torrent_name[26] == " ":
torrent_name = torrent_name[:26]+"...."
else:
torrent_name = torrent_name[:27]+"..."
while len(torrent_name) < 30:
torrent_name += " "
if torrent["size"] == 0:
download_ratio = 0
elif torrent["amount_left"] == 0:
download_ratio = 1
else:
download_ratio = min(
torrent["downloaded"]/torrent["size"],
1
)
progress_bar = "|"+(""*floor(download_ratio*20))
while len(progress_bar) < 21:
progress_bar += " "
progress_bar += "| "+str(floor(download_ratio*100))+"%"
while len(progress_bar) < 27:
progress_bar += " "
eta_in_seconds = torrent["eta"]
if eta_in_seconds >= 8640000:
eta = ""
else:
eta = ""
if eta_in_seconds >= 86400:
eta += str(floor(eta_in_seconds/86400))+"d "
if eta_in_seconds >= 3600:
eta += str(floor((eta_in_seconds%86400)/3600))+"h "
if eta_in_seconds >= 60:
eta += str(floor((eta_in_seconds%3600)/60))+"m "
eta += str(eta_in_seconds%60)+"s"
torrent_info = f"{torrent_name} {progress_bar} "
torrent_info += f"(Eta: {eta})"
if torrent["state"] == "stalledDL":
torrent_info += " (Stalled)"
if not (download_ratio == 1 and
torrent["last_activity"] < time.time()-7200):
message.append(torrent_info)
if download_ratio < 1 and torrent["state"] != "stalledDL":
all_downloaded = False
else:
message.append("No torrents currently downloading")
if show_movies:
message.append("")
movies_section_title = "*Missing movies not downloading*"
movies_section_line = (
"-"*((title_width-len(movies_section_title))//2)
)
message.append(
movies_section_line+movies_section_title+movies_section_line
)
movie_list = requests.get(
self.radarr_url+"movie?apiKey="+self.credentials["radarr_key"]
).json()
print(
self.radarr_url+"movie?apiKey="+self.credentials["radarr_key"]
)
movie_queue = requests.get(
self.radarr_url+"queue?apiKey="+self.credentials["radarr_key"]
).json()
movie_queue_ids = []
for queue_item in movie_queue["records"]:
movie_queue_ids.append(queue_item["movieId"])
for movie in movie_list:
if (not movie["hasFile"] and
movie["id"] not in movie_queue_ids):
movie_name = movie["title"]
if len(movie_name) > 40:
if movie_name[36] == " ":
movie_name = movie_name[:36]+"...."
else:
movie_name = movie_name[:37]+"..."
while len(movie_name) < 41:
movie_name += " "
if movie["monitored"]:
movie_info = movie_name+"Could not find a torrent"
else:
movie_info = self.long_strings["No torrent"].format(
movie_name
)
message.append(movie_info)
if show_shows:
message.append("")
show_section_title = "*Missing shows not downloading*"
show_section_line = "-"*((title_width-len(show_section_title))//2)
message.append(
show_section_line+show_section_title+show_section_line
)
show_list = requests.get(
self.sonarr_url+"series?apiKey="+self.credentials["sonarr_key"]
).json()
for show in show_list:
if show["seasons"][0]["seasonNumber"] == 0:
seasons = show["seasons"][1:]
else:
seasons = show["seasons"]
if any(
(
i["statistics"]["episodeCount"] !=
i["statistics"]["totalEpisodeCount"]
) for i in seasons):
if all(
i["statistics"]["episodeCount"] == 0 for i in seasons
):
message.append(show["title"] + " (all episodes)")
else:
if episodes:
missing_episodes = sum(
(i["statistics"]["totalEpisodeCount"] -
i["statistics"]["episodeCount"])
for i in seasons)
message.append(
f"{show['title']} ({missing_episodes} episodes)"
)
message.append("-"*title_width)
message_text = "```"+"\n".join(message[1:])+"```"
if message_text == "``````":
message_text = self.long_strings["No torrents downloading"]
return message_text, all_downloaded
async def downloading(self, ctx, content):
"""Send message with list of all downloading torrents."""
async def send_long_message(ctx,message_text):
if len(message_text) <= 1994:
await ctx.send("```"+message_text+"```")
else:
cut_off_index = message_text[:1994].rfind("\n")
await ctx.send("```"+message_text[:cut_off_index]+"```")
await send_long_message(ctx,message_text[cut_off_index+1:])
await self.bot.defer(ctx)
# showDM, showMovies, showShows, episodes
parameters = [False, False, False, False]
show_dm_args = ["d", "dm", "downloading", "downloadmanager"]
show_movies_args = ["m", "movies"]
show_shows_args = ["s", "shows", "series"]
show_episode_args = ["e", "episodes"]
arg_list = [
show_dm_args, show_movies_args, show_shows_args, show_episode_args
]
input_args = []
valid_arguments = True
while content != "" and valid_arguments:
if content[0] == " ":
content = content[1:]
elif content[0] == "-":
if content[1] == "-":
arg_start = 2
if " " in content:
arg_stop = content.find(" ")
else:
arg_stop = None
else:
arg_start = 1
arg_stop = 2
input_args.append(content[arg_start:arg_stop])
if arg_stop is None:
content = ""
else:
content = content[arg_stop:]
else:
valid_arguments = False
if valid_arguments:
for arg_index, arg_aliases in enumerate(arg_list):
arg_in_input = [i in input_args for i in arg_aliases]
if any(arg_in_input):
input_args.remove(arg_aliases[arg_in_input.index(True)])
parameters[arg_index] = True
if len(input_args) != 0 or (not parameters[2] and parameters[3]):
valid_arguments = False
show_anything = any(i for i in parameters)
if not (valid_arguments and show_anything):
await ctx.send(self.long_strings["Invalid parameters"])
else:
message_text, all_downloaded = await self.__generate_download_list(
*parameters
)
if not message_text.startswith("```"):
await ctx.send(message_text)
elif len(message_text) > 2000:
message_text = message_text[3:-3]
await send_long_message(ctx,message_text)
elif all_downloaded:
await ctx.send(message_text)
else:
updates_left = 60
message_text = self.long_strings["Update"].format(
message_text[:-3], ceil(updates_left/6)
)
old_message = await ctx.send(message_text)
while ((not all_downloaded) and updates_left > 0):
await asyncio.sleep(10)
updates_left -= 1
message_text, all_downloaded = await (
self.__generate_download_list(*parameters)
)
message_text = self.long_strings["Update"].format(
message_text[:-3],
ceil(updates_left/6)
)
await old_message.edit(content = message_text)
message_text, all_downloaded = await (
self.__generate_download_list(*parameters)
)
if message_text.startswith("```"):
if all_downloaded:
self.bot.log("All torrents are downloaded")
else:
message_text = self.long_strings["No updates"].format(
message_text[:-3]
)
self.bot.log("The message updated 20 times")
await old_message.edit(content = message_text)

View File

@@ -0,0 +1 @@
from .roll import DieRoller

View File

@@ -0,0 +1,737 @@
from __future__ import annotations
from random import randint
from rply.token import BaseBox
def show_exp(exp, vtable):
r = exp.show(vtable)
if type(exp) not in [ExpInt,ExpRoll,ExpVar]:
r = f"({r})"
return r
class Exp(BaseBox):
def eval(self, vtable, *args):
return self._eval(vtable.copy(), *args)
def show(self, vtable, *args):
return self._show(vtable.copy(), *args)
def _eval(self, _):
return None
def _show(self, _):
return ""
def includes(self, _):
return False
def __repr__(self) -> str:
return "exp()"
def __eq__(self, other: Exp) -> bool:
return False
class ExpInt(Exp):
def __init__(self, value: int):
self.value = value
def _eval(self, _):
return self.value
def _show(self, _):
return str(self.value)
def includes(self, _):
return False
def __repr__(self) -> str:
return f"exp_int({self.value})"
def __eq__(self, other: Exp) -> bool:
return isinstance(other, ExpInt) and self.value == other.value
class ExpMin(Exp):
def __init__(self, exp1: Exp, exp2: Exp):
self.exp1 = exp1
self.exp2 = exp2
def _eval(self, vtable):
r1 = self.exp1.eval(vtable)
r2 = self.exp2.eval(vtable)
if not (isinstance(r1,int) and isinstance(r2,int)):
return None
return max(r1, r2)
def _show(self, vtable):
r1 = show_exp(self.exp1, vtable)
r2 = show_exp(self.exp2, vtable)
return f"{r1}min{r2}"
def includes(self, var: str):
return self.exp1.includes(var) or self.exp2.includes(var)
def __repr__(self) -> str:
return f"exp_min({self.exp1},{self.exp2})"
def __eq__(self, other: Exp) -> bool:
return (
isinstance(other, ExpMin) and
self.exp1 == other.exp1 and
self.exp2 == other.exp2
)
class ExpMax(Exp):
def __init__(self, exp1: Exp, exp2: Exp):
self.exp1 = exp1
self.exp2 = exp2
def _eval(self, vtable):
r1 = self.exp1.eval(vtable)
r2 = self.exp2.eval(vtable)
if not (isinstance(r1,int) and isinstance(r2,int)):
return None
return min(r1, r2)
def _show(self, vtable):
r1 = show_exp(self.exp1, vtable)
r2 = show_exp(self.exp2, vtable)
return f"{r1}max{r2}"
def includes(self, var: str):
return self.exp1.includes(var) or self.exp2.includes(var)
def __repr__(self) -> str:
return f"exp_max({self.exp1},{self.exp2})"
def __eq__(self, other: Exp) -> bool:
return (
isinstance(other, ExpMax) and
self.exp1 == other.exp1 and
self.exp2 == other.exp2
)
class ExpBinop(Exp):
def __init__(self, op: str, exp1: Exp, exp2: Exp):
self.op = op
self.exp1 = exp1
self.exp2 = exp2
def _eval(self, vtable):
r1 = self.exp1.eval(vtable)
r2 = self.exp2.eval(vtable)
if not (isinstance(r1,int) and isinstance(r2,int)):
return None
if self.op == "+":
return r1+r2
elif self.op == "-":
return r1-r2
elif self.op == "*":
return r1*r2
elif self.op == "/":
return r1//r2
else:
raise Exception(f"Unknown binop {self.op}")
def _show(self, vtable):
r1 = show_exp(self.exp1, vtable)
r2 = show_exp(self.exp2, vtable)
return f"{r1}{self.op}{r2}"
def includes(self, var: str):
return self.exp1.includes(var) or self.exp2.includes(var)
def __repr__(self) -> str:
return f"exp_binop({self.op}, {self.exp1}, {self.exp2})"
def __eq__(self, other: Exp) -> bool:
return (
isinstance(other, ExpBinop) and
self.op == other.op and
self.exp1 == other.exp1 and
self.exp2 == other.exp2
)
class ExpNeg(Exp):
def __init__(self, exp: Exp):
self.exp = exp
def _eval(self, vtable):
r = self.exp.eval(vtable)
if not isinstance(r,int):
return None
return -r
def _show(self, vtable):
r = show_exp(self.exp, vtable)
return f"-{r}"
def includes(self, var: str):
return self.exp.includes(var)
def __repr__(self) -> str:
return f"exp_neg({self.exp})"
def __eq__(self, other: Exp) -> bool:
return (
isinstance(other, ExpNeg) and
self.exp == other.exp
)
class ExpLet(Exp):
def __init__(self, var: str, exp1: Exp, exp2: Exp):
self.var = var
self.exp1 = exp1
self.exp2 = exp2
def _eval(self, vtable):
r1 = self.exp1.eval(vtable)
vtable[self.var] = r1
return self.exp2.eval(vtable)
def _show(self, vtable):
r1 = show_exp(self.exp1, vtable)
vtable[self.var] = self.exp1.eval(vtable)
r2 = show_exp(self.exp2, vtable)
if self.exp2.includes(self.var):
return f"let {self.var}={r1} in {r2}"
else:
return r2
def includes(self, var: str):
return self.exp1.includes(var) or self.exp2.includes(var)
def __repr__(self) -> str:
return f"exp_let({self.var}, {self.exp1}, {self.exp2})"
def __eq__(self, other: Exp) -> bool:
return (
isinstance(other, ExpLet) and
self.var == other.var and
self.exp1 == other.exp1 and
self.exp2 == other.exp2
)
class ExpIf(Exp):
def __init__(self, exp1: Exp, exp2: Exp, exp3: Exp):
self.exp1 = exp1
self.exp2 = exp2
self.exp3 = exp3
def _eval(self, vtable):
r1 = self.exp1.eval(vtable)
if r1 > 0:
return self.exp2.eval(vtable)
else:
return self.exp3.eval(vtable)
def _show(self, vtable):
r1 = show_exp(self.exp1, vtable)
r2 = show_exp(self.exp2, vtable)
r3 = self.exp3.show(vtable)
return f"if {r1} then {r2} else {r3}"
def includes(self, var: str):
return self.exp1.includes(var) or self.exp2.includes(var) or self.exp3.includes(var)
def __repr__(self) -> str:
return f"exp_if({self.exp1}, {self.exp2}, {self.exp3})"
def __eq__(self, other: Exp) -> bool:
return (
isinstance(other, ExpIf) and
self.exp1 == other.exp1 and
self.exp2 == other.exp2 and
self.exp3 == other.exp3
)
class ExpLambda(Exp):
def __init__(self, var: str, exp: Exp):
self.var = var
self.exp = exp
def _eval(self, _):
return (self.exp, self.var)
def _show(self, vtable):
r = show_exp(self.exp, vtable)
return f"\\{self.var} -> {r}"
def includes(self, var: str):
return self.exp.includes(var)
def __repr__(self) -> str:
return f"exp_lambda({self.var}, {self.exp})"
def __eq__(self, other: Exp) -> bool:
return (
isinstance(other, ExpLambda) and
self.var == other.var and
self.exp == other.exp
)
class ExpApply(Exp):
def __init__(self, exp1: Exp, exp2: Exp):
self.exp1 = exp1
self.exp2 = exp2
def _eval(self, vtable):
r1 = self.exp1.eval(vtable)
if isinstance(r1, tuple):
r2 = self.exp2.eval(vtable)
vtable[r1[1]] = r2
return r1[0].eval(vtable)
else:
return None
def _show(self, vtable):
r1 = show_exp(self.exp1, vtable)
r2 = self.exp2.show(vtable)
return f"{r1}({r2})"
def includes(self, var: str):
return self.exp1.includes(var) or self.exp2.includes(var)
def __repr__(self) -> str:
return f"exp_apply({self.exp1}, {self.exp2})"
def __eq__(self, other: Exp) -> bool:
return (
isinstance(other, ExpApply) and
self.exp1 == other.exp1 and
self.exp2 == other.exp2
)
class ExpVar(Exp):
def __init__(self, var: str):
self.var = var
def _eval(self, vtable):
return vtable[self.var] if self.var in vtable else None
def _show(self, vtable):
return self.var
def includes(self, var: str):
return var == self.var
def __repr__(self) -> str:
return f"exp_var({self.var})"
def __eq__(self, other: Exp) -> bool:
return (
isinstance(other, ExpVar) and
self.var == other.var
)
class ComparePoint(Exp):
def __init__(self, comp_op: str, exp: Exp) -> None:
self.comp_op = comp_op
self.exp = exp
def _eval(self, vtable, val: int):
r = self.exp.eval(vtable)
if not isinstance(r,int):
return None
if self.comp_op == "=":
return 1 if val == r else 0
if self.comp_op == "<":
return 1 if val < r else 0
if self.comp_op == "<=":
return 1 if val <= r else 0
if self.comp_op == ">":
return 1 if val <= r else 0
if self.comp_op == ">=":
return 1 if val <= r else 0
else:
raise Exception(f"Unknown binop {self.op}")
def _show(self, vtable):
r = show_exp(self.exp, vtable)
return f"{self.comp_op}{r}"
def includes(self, var: str):
return self.exp.includes(var)
def __repr__(self) -> str:
return f"comp({self.comp_op},{self.exp})"
def __eq__(self, other: Exp) -> bool:
return (
isinstance(other, ComparePoint) and
self.comp_op == other.comp_op and
self.exp == other.exp
)
class ExpTest(Exp):
def __init__(self, exp: Exp, comp: ComparePoint) -> None:
self.exp = exp
self.comp = comp
def _eval(self, vtable):
r = self.exp.eval(vtable)
return self.comp.eval(vtable, r)
def _show(self, vtable):
r = show_exp(self.exp, vtable)
c = self.comp.show(vtable)
return f"{r}{c}"
def includes(self, var: str):
return self.exp.includes(var) or self.comp.includes(var)
def __repr__(self) -> str:
return f"test({self.exp},{self.comp})"
def __eq__(self, other: Exp) -> bool:
return (
isinstance(other, ExpTest) and
self.exp == other.exp and
self.comp == other.comp
)
class ExpRoll(Exp):
def __init__(self, roll: Roll):
self.roll = roll
def _eval(self, vtable):
return sum(self.roll.eval(vtable))
def _show(self, vtable):
return f"[{','.join(self.roll.show(vtable))}]"
def includes(self, _):
return False
def __repr__(self) -> str:
return f"sum({self.roll})"
def __eq__(self, other: Exp) -> bool:
return (
isinstance(other, ExpRoll) and
self.roll == other.roll
)
class Roll(Exp):
def __init__(self, exp1: Exp, exp2: Exp):
self.exp1 = exp1
self.exp2 = exp2
self.result = None
def _eval(self, vtable):
if self.result is not None:
return self.result
r1 = self.exp1.eval(vtable)
r2 = self.exp2.eval(vtable)
if not (isinstance(r1,int) and isinstance(r2,int)):
return []
self.die_type = r2
self.result = [randint(1,r2) for _ in range(r1)]
self.show_list = [str(i) for i in self.result]
return self.result
def _show(self, vtable):
self.eval(vtable)
return self.show_list
@property
def die(self) -> int:
if hasattr(self, "die_type"):
return self.die_type
elif hasattr(self, "roll"):
return self.roll.die_type
else:
return 0
def __repr__(self) -> str:
return f"roll({self.exp1},{self.exp2})"
def __eq__(self, other: Exp) -> bool:
return (
isinstance(other, Roll) and
self.exp1 == other.exp1 and
self.exp2 == other.exp2
)
class RollKeepHighest(Roll):
def __init__(self, roll: Roll, exp: Exp):
self.roll = roll
self.exp = exp
self.result = None
self.show_list = None
def _eval(self, vtable):
if self.result is not None:
return self.result
r1 = self.roll.eval(vtable)
r2 = self.exp.eval(vtable)
if not ((isinstance(r1,list) and all(isinstance(i,int) for i in r1)) and isinstance(r2,int)):
return []
max_indices = []
for i, n in enumerate(r1):
if len(max_indices) < r2:
max_indices.append(i)
elif not all([n <= r1[j] for j in max_indices]):
max_indices.remove(min(max_indices,key=lambda x: r1[x]))
max_indices.append(i)
self.result = [r1[i] for i in max_indices]
self.show_list = [str(n) if i in max_indices else f"~~{n}~~" for i, n in enumerate(r1)]
return self.result
def __repr__(self) -> str:
return f"kep_highest({self.roll},{self.exp})"
def __eq__(self, other: Exp) -> bool:
return (
isinstance(other, RollKeepHighest) and
self.roll == other.roll and
self.exp == other.exp
)
class RollKeepLowest(Roll):
def __init__(self, roll: Roll, exp: Exp):
self.roll = roll
self.exp = exp
self.result = None
self.show_list = None
def _eval(self, vtable):
if self.result is not None:
return self.result
r1 = self.roll.eval(vtable)
r2 = self.exp.eval(vtable)
if not ((isinstance(r1,list) and all(isinstance(i,int) for i in r1)) and isinstance(r2,int)):
return []
min_indices = []
for i, n in enumerate(r1):
if len(min_indices) < r2:
min_indices.append(i)
elif not all([n >= r1[j] for j in min_indices]):
min_indices.remove(max(min_indices,key=lambda x: r1[x]))
min_indices.append(i)
self.result = [r1[i] for i in min_indices]
self.show_list = [str(n) if i in min_indices else f"~~{n}~~" for i, n in enumerate(r1)]
return self.result
def __repr__(self) -> str:
return f"kep_lowest({self.roll},{self.exp})"
def __eq__(self, other: Exp) -> bool:
return (
isinstance(other, RollKeepLowest) and
self.roll == other.roll and
self.exp == other.exp
)
class RollMin(Roll):
def __init__(self, roll: Roll, exp: Exp):
self.roll = roll
self.exp = exp
self.result = None
self.show_list = None
def _eval(self, vtable):
if self.result is not None:
return self.result
r1 = self.roll.eval(vtable)
r2 = self.exp.eval(vtable)
if not ((isinstance(r1,list) and all(isinstance(i,int) for i in r1)) and isinstance(r2,int)):
return []
self.show_list = []
for i in range(len(r1)):
if r1[i] < r2:
r1[i] = r2
self.show_list.append(f"{r2}^")
else:
self.show_list.append(str(r1[i]))
self.result = r1
return self.result
def __repr__(self) -> str:
return f"min({self.roll},{self.exp})"
def __eq__(self, other: Exp) -> bool:
return (
isinstance(other, RollMin) and
self.roll == other.roll and
self.exp == other.exp
)
class RollMax(Roll):
def __init__(self, roll: Roll, exp: Exp):
self.roll = roll
self.exp = exp
self.result = None
self.show_list = None
def _eval(self, vtable):
if self.result is not None:
return self.result
r1 = self.roll.eval(vtable)
r2 = self.exp.eval(vtable)
if not ((isinstance(r1,list) and all(isinstance(i,int) for i in r1)) and isinstance(r2,int)):
return []
self.show_list = []
for i in range(len(r1)):
if r1[i] > r2:
r1[i] = r2
self.show_list.append(f"{r2}v")
else:
self.show_list.append(str(r1[i]))
self.result = r1
return self.result
def __repr__(self) -> str:
return f"max({self.roll},{self.exp})"
def __eq__(self, other: Exp) -> bool:
return (
isinstance(other, RollMax) and
self.roll == other.roll and
self.exp == other.exp
)
class RollExplode(Roll):
def __init__(self, roll: Roll, comp: ComparePoint = None):
self.roll = roll
self.comp = comp
self.result = None
self.show_list = None
def _eval(self, vtable):
if self.result is not None:
return self.result
r1 = self.roll.eval(vtable)
if not (isinstance(r1,list) and all(isinstance(i,int) for i in r1)):
return []
d = self.die
if self.comp is None:
self.comp = ComparePoint("=", ExpInt(d))
self.result = []
self.show_list = []
def compare(n):
if self.comp.eval(vtable, n):
self.result.append(n)
self.show_list.append(f"{n}!")
compare(randint(1,d))
else:
self.result.append(n)
self.show_list.append(str(n))
for n in r1:
compare(n)
return self.result
def __repr__(self) -> str:
return f"max({self.roll},{self.exp})"
def __eq__(self, other: Exp) -> bool:
return (
isinstance(other, RollMax) and
self.roll == other.roll and
self.exp == other.exp
)
class RollReroll(Roll):
def __init__(self, roll: Roll, comp: ComparePoint = None, once: bool = False):
self.roll = roll
self.comp = comp
self.once = once
self.result = None
self.show_list = None
def _eval(self, vtable):
if self.result is not None:
return self.result
r1 = self.roll.eval(vtable)
if not (isinstance(r1,list) and all(isinstance(i,int) for i in r1)):
return []
d = self.die
if self.comp is None:
self.comp = ComparePoint("=", ExpInt(1))
self.result = []
self.show_list = []
def compare(n, rerolled):
if self.comp.eval(vtable, n) and not (rerolled and self.once):
self.show_list.append(f"~~{n}~~")
compare(randint(1,d), True)
else:
self.result.append(n)
self.show_list.append(str(n))
for n in r1:
compare(n, False)
return self.result
def __repr__(self) -> str:
return f"max({self.roll},{self.exp})"
def __eq__(self, other: Exp) -> bool:
return (
isinstance(other, RollMax) and
self.roll == other.roll and
self.exp == other.exp
)

View File

@@ -0,0 +1,52 @@
from string import ascii_letters, digits
from rply import LexerGenerator
VALID_CHARACTERS = ascii_letters+"_"+digits
TOKENS = [
("ROLL_DIE", r"d"),
("ROLL_KEEP_LOWEST", r"kl"),
("ROLL_KEEP_HIGHEST", r"kh?"),
("ROLL_REROLL_ONCE", r"ro"),
("ROLL_REROLL", r"r"),
("ROLL_MIN", r"min"),
("ROLL_MAX", r"max"),
("ROLL_EXPLODE", r"!"),
("SYMBOL_LE", r"\<\="),
("SYMBOL_GE", r"\>\="),
("SYMBOL_LT", r"\<"),
("SYMBOL_GT", r"\>"),
("SYMBOL_EQUALS", r"\="),
("SYMBOL_ARROW", r"\-\>"),
("SYMBOL_BACKSLASH", r"\\"),
("SYMBOL_LPARENS", r"\("),
("SYMBOL_RPARENS", r"\)"),
("SYMBOL_PLUS", r"\+"),
("SYMBOL_MINUS", r"\-"),
("SYMBOL_TIMES", r"\*"),
("SYMBOL_DIVIDE", r"\/"),
("KEYWORD_LET", r"let"),
("KEYWORD_IN", r"in"),
("KEYWORD_IF", r"if"),
("KEYWORD_THEN", r"then"),
("KEYWORD_ELSE", r"else"),
("INT", r"\d+"),
("ID", f"[{ascii_letters}][{VALID_CHARACTERS}]*")
]
class Lexer():
def __init__(self):
self.lexer = LexerGenerator()
def _add_tokens(self):
for token in TOKENS:
self.lexer.add(*token)
self.lexer.ignore(r"\s+")
def get_lexer(self):
self._add_tokens()
return self.lexer.build()

View File

@@ -0,0 +1,153 @@
from rply import ParserGenerator
from lexer import TOKENS
from ast_nodes import Exp, ExpInt, ExpBinop, ExpLet, ExpVar, ExpMin, ExpMax, ExpRoll, Roll, RollKeepHighest, RollKeepLowest, RollMin, RollMax, RollExplode, ComparePoint, ExpIf, ExpTest, ExpApply, ExpLambda, ExpNeg, RollReroll
class Parser():
def __init__(self):
self.pg = ParserGenerator(
[i[0] for i in TOKENS],
precedence=[
('left', ["SYMBOL_BACKSLASH","SYMBOL_ARROW"]),
('left', ["KEYWORD_LET", "KEYWORD_IN", "KEYWORD_IF", "KEYWORD_THEN", "KEYWORD_ELSE"]),
('left', ["SYMBOL_EQUALS", "SYMBOL_LT", "SYMBOL_LE", "SYMBOL_GT", "SYMBOL_GE"]),
('left', ["SYMBOL_PLUS", "SYMBOL_MINUS"]),
('left', ["SYMBOL_TIMES", "SYMBOL_DIVIDE"]),
('left', ["ROLL_DIE"]),
('right', ["ROLL_KEEP_HIGHEST","ROLL_KEEP_LOWEST","ROLL_MIN","ROLL_MAX","ROLL_EXPLODE","ROLL_REROLL","ROLL_REROLL_ONCE"])
]
)
self._get_parser()
def parse(self, token_input):
return self.parser.parse(token_input)
def _get_parser(self) -> Exp:
# Expressions
@self.pg.production('exp : exp SYMBOL_PLUS exp')
@self.pg.production('exp : exp SYMBOL_MINUS exp')
@self.pg.production('exp : exp SYMBOL_TIMES exp')
@self.pg.production('exp : exp SYMBOL_DIVIDE exp')
def exp_mul_div(tokens):
return ExpBinop(tokens[1].value, tokens[0], tokens[2])
@self.pg.production('exp : atom')
def exp_atom(tokens):
return tokens[0]
@self.pg.production("exp : SYMBOL_MINUS atom")
def exp_neg(tokens):
return ExpNeg(tokens[1])
@self.pg.production('exp : KEYWORD_LET ID SYMBOL_EQUALS exp KEYWORD_IN exp')
def exp_let(tokens):
return ExpLet(tokens[1].value, tokens[3], tokens[5])
@self.pg.production('exp : roll')
def exp_roll(tokens):
return ExpRoll(tokens[0])
@self.pg.production("exp : atom ROLL_MIN atom")
def exp_min(tokens):
return ExpMin(tokens[0], tokens[2])
@self.pg.production("exp : atom ROLL_MAX atom")
def exp_max(tokens):
return ExpMax(tokens[0], tokens[2])
@self.pg.production("exp : atom comp")
def exp_test(tokens):
return ExpTest(tokens[0], tokens[1])
@self.pg.production("exp : KEYWORD_IF exp KEYWORD_THEN exp KEYWORD_ELSE exp")
def exp_if(tokens):
return ExpIf(tokens[1],tokens[3],tokens[5])
@self.pg.production("exp : SYMBOL_BACKSLASH ID SYMBOL_ARROW exp")
def exp_lamda(tokens):
return ExpLambda(tokens[1].value, tokens[3])
@self.pg.production("exp : atom atom")
def apply(tokens):
return ExpApply(tokens[0],tokens[1])
# Rolls
@self.pg.production('roll : roll ROLL_KEEP_HIGHEST atom')
def roll_keep_highest(tokens):
return RollKeepHighest(tokens[0], tokens[2])
@self.pg.production('roll : roll ROLL_KEEP_LOWEST atom')
def roll_keep_lowest(tokens):
return RollKeepLowest(tokens[0], tokens[2])
@self.pg.production('roll : roll ROLL_MIN atom')
def roll_min(tokens):
return RollMin(tokens[0], tokens[2])
@self.pg.production('roll : roll ROLL_MAX atom')
def roll_max(tokens):
return RollMax(tokens[0], tokens[2])
@self.pg.production('roll : roll ROLL_EXPLODE ')
def roll_explode(tokens):
return RollExplode(tokens[0])
@self.pg.production('roll : roll ROLL_EXPLODE comp')
def roll_explode_comp(tokens):
return RollExplode(tokens[0], tokens[2])
@self.pg.production('roll : roll ROLL_REROLL ')
def roll_reroll(tokens):
return RollReroll(tokens[0])
@self.pg.production('roll : roll ROLL_REROLL comp')
def roll_reroll_comp(tokens):
return RollReroll(tokens[0], tokens[2])
@self.pg.production('roll : roll ROLL_REROLL_ONCE')
def roll_reroll_once(tokens):
return RollReroll(tokens[0], None, True)
@self.pg.production('roll : roll ROLL_REROLL_ONCE comp')
def roll_reroll_once_comp(tokens):
return RollReroll(tokens[0], tokens[2], True)
@self.pg.production('roll : atom ROLL_DIE atom')
def roll(tokens):
return Roll(tokens[0], tokens[2])
@self.pg.production('roll : ROLL_DIE atom')
def roll_no_amount(tokens):
return Roll(ExpInt(1), tokens[1])
# Compare Points
@self.pg.production("comp : SYMBOL_EQUALS atom")
@self.pg.production("comp : SYMBOL_LT atom")
@self.pg.production("comp : SYMBOL_LE atom")
@self.pg.production("comp : SYMBOL_GT atom")
@self.pg.production("comp : SYMBOL_GE atom")
def comp_point(tokens):
return ComparePoint(tokens[0].value,tokens[1])
# Atoms
@self.pg.production("atom : INT")
def atom_int(tokens):
return ExpInt(int(tokens[0].value))
@self.pg.production("atom : SYMBOL_LPARENS exp SYMBOL_RPARENS")
def atom_exp(tokens):
return tokens[1]
@self.pg.production('atom : ID')
def atom_var(tokens):
return ExpVar(tokens[0].value)
## Error Handling ##
@self.pg.error
def error_handle(token):
raise Exception(f"Unexpected token '{token.value}' ({token.name}) at line {token.source_pos.lineno}, column {token.source_pos.colno}.")
## Finish ##
self.parser = self.pg.build()

View File

@@ -0,0 +1,58 @@
import os, sys; sys.path.append(os.path.dirname(os.path.realpath(__file__)))
from lexer import Lexer
from parser import Parser
from ast_nodes import Exp, ExpInt, ExpBinop, ExpLet, ExpVar, ExpRoll, Roll, RollKeepHighest, RollKeepLowest, RollMin, RollMax, ExpMin, ExpMax
class DieRoller():
def __init__(self) -> None:
self.lexer = Lexer().get_lexer()
self.parser = Parser()
def parse(self, string: str) -> Exp:
tokens = self.lexer.lex(string)
expression = self.parser.parse(tokens)
return expression
def roll(self, string: str):
exp = self.parse(string)
# print(exp)
# exit()
show = exp.show({})
eval = exp.eval({})
if show is None or eval is None or isinstance(eval,tuple):
raise Exception("Something went wrong")
return f"**Result**: {show}\n**Total**: {eval}"
if __name__ == "__main__":
d = DieRoller()
test_strings = [
("4", ExpInt(4)),
("1 + 1", ExpBinop("+",ExpInt(1),ExpInt(1))),
("5*5", ExpBinop("*",ExpInt(5),ExpInt(5))),
("1+5/5", ExpBinop("+",ExpInt(1),ExpBinop("/",ExpInt(5),ExpInt(5)))),
("(1+5)/6", ExpBinop("/",ExpBinop("+",ExpInt(1),ExpInt(5)),ExpInt(6))),
("let x=5 in x*2", ExpLet("x",ExpInt(5),ExpBinop("*",ExpVar("x"),ExpInt(2)))),
("let x= let y = 2 in y in let z = 5 in x*z", ExpLet("x",ExpLet("y",ExpInt(2),ExpVar("y")),ExpLet("z",ExpInt(5),ExpBinop("*",ExpVar("x"),ExpVar("z"))))),
("4d6", ExpRoll(Roll(ExpInt(4),ExpInt(6)))),
("d6", ExpRoll(Roll(ExpInt(1),ExpInt(6)))),
("(2+2)d(3*2)", ExpRoll(Roll(ExpBinop("+",ExpInt(2),ExpInt(2)),ExpBinop("*",ExpInt(3),ExpInt(2))))),
("4d3*2", ExpBinop("*",ExpRoll(Roll(ExpInt(4),ExpInt(3))),ExpInt(2))),
("4d6kh3", ExpRoll(RollKeepHighest(Roll(ExpInt(4),ExpInt(6)),ExpInt(3)))),
("2d20kl1", ExpRoll(RollKeepLowest(Roll(ExpInt(2),ExpInt(20)),ExpInt(1)))),
("11d10kh6kl1", ExpRoll(RollKeepLowest(RollKeepHighest(Roll(ExpInt(11),ExpInt(10)),ExpInt(6)),ExpInt(1)))),
("3d6min6", ExpRoll(RollMin(Roll(ExpInt(3),ExpInt(6)),ExpInt(6)))),
("3d6max1", ExpRoll(RollMax(Roll(ExpInt(3),ExpInt(6)),ExpInt(1)))),
("let x = 1d4 in (x)min3", ExpLet("x",ExpRoll(Roll(ExpInt(1),ExpInt(4))),ExpMin(ExpVar("x"),ExpInt(3)))),
("10max(2d10)",ExpMax(ExpInt(10),ExpRoll(Roll(ExpInt(2),ExpInt(10)))))
]
for s, t in test_strings:
r = d.parse(s)
correct = r == t
print(f"{str(s) : <20}: {'' if correct else ''} ({r.eval({})})")
if not correct:
print(f"Expected: {t}")
print(f"Actual: {r}\n")
print(d.roll(input(":")))

View File

@@ -1,10 +0,0 @@
from .star_wars_char import StarWarsChar
from .star_wars_roll import StarWarsRoll
from .star_wars_destiny import StarWarsDestiny
class StarWars():
def __init__(self, bot):
self.bot = bot
self.character = StarWarsChar(self.bot)
self.roll = StarWarsRoll(self.bot)
self.destiny = StarWarsDestiny(self.bot)

View File

@@ -1,528 +0,0 @@
import json
import string
import discord
class StarWarsChar():
def __init__(self, bot):
self.bot = bot
def get_char_name(self, user : str):
self.bot.log("Getting name for "+self.bot.database_funcs.get_name(user)+"'s character")
user_character = self.bot.database["starwars characters"].find_one({"_id":user})
if user_character != None:
self.bot.log("Name is "+user_character["Name"])
return user_character["Name"]
else:
self.bot.log("Just using "+self.bot.database_funcs.get_name(user))
return self.bot.database_funcs.get_name(user)
def set_up_dict(self, cmd : dict):
self.bot.log("Setting up a dictionary in a nice way")
if bool(cmd):
keys = list(cmd)
values = list(cmd.values())
result = ""
if isinstance(values[0],dict):
return ", ".join(values)
else:
for i, key in enumerate(keys):
if type(key) is list:
if i%3 != 2:
result += "**" + key + "**" + ": " + ", ".join(values[i]) + " "
else:
result += "**" + key + "**" + ": " + ", ".join(values[i]) + "\n"
else:
if i%3 != 2:
result += "**" + key + "**" + ": " + str(values[i]) + " "
else:
result += "**" + key + "**" + ": " + str(values[i]) + "\n"
self.bot.log("Returning a dictionary, but well formatted")
return result
else:
self.bot.log("Couldn't find anything")
return "There doesn't seem to be anything here..."
def look_up(self, data : dict, key : str, cmd : str = ""):
if cmd == " ":
cmd = ""
elif cmd != "":
while cmd[0] == " ":
cmd = cmd[1:]
if cmd == "":
break
self.bot.log("Looking up "+key)
if key in data:
self.bot.log(key+" exists")
if cmd == "":
if type(data[key]) is dict and key != "Weapons":
return self.set_up_dict(data[key])
elif key == "Weapons":
self.bot.log("Does this even get used? I'm too scared to delete it")
if bool(data[key]):
self.bot.log("Found "+(", ".join(list(data[key]))))
return ", ".join(list(data[key]))
else:
self.bot.log("There is nothing here")
return "There doesn't seem to be anything here..."
else:
if str(data[key]) != "":
self.bot.log("Returning "+str(data[key]))
return data[key]
else:
self.bot.log("There was nothing there")
return "There doesn't seem to be anything here"
elif cmd[0] == '+':
self.bot.log("Trying to add to "+key)
try:
cmd = cmd[1:]
while cmd[0] == ' ':
cmd = cmd[1:]
except:
self.bot.log("Yeah, that fucked up")
return "Can't do that"
if type(data[key]) is int:
try:
new_value = data[key] + int(cmd)
data[key] = new_value
self.bot.log("Added "+cmd+" to "+key)
return data
except:
self.bot.log("Couldn't add "+cmd+" to "+key)
return "Can't add that"
elif type(data[key]) is list:
try:
data[key].append(cmd)
self.bot.log("Added "+cmd+" to "+key)
return data
except:
self.bot.log("Couldn't add "+cmd+" to "+key)
return "Can't add that"
else:
self.bot.log("Yeah, I can't add that to "+key)
return "Can't add that"
elif cmd[0] == '-':
self.bot.log("Trying to remove/subtract from "+key)
try:
cmd = cmd[1:]
while cmd[0] == ' ':
cmd = cmd[1:]
except:
self.bot.log("Yeah, that fucked up")
return "Can't do that"
if type(data[key]) is int:
try:
new_value = data[key] - int(cmd)
data[key] = new_value
self.bot.log("Subtracted "+cmd+" from "+key)
return data
except:
self.bot.log("Couldn't subtract "+cmd+" from "+key)
return "Can't remove that"
elif type(data[key]) is list:
try:
data[key].remove(cmd)
self.bot.log("Removed "+cmd+" from "+key)
return data
except:
self.bot.log("Couldn't remove "+cmd+" from "+key)
return "Can't remove that"
else:
self.bot.log("Yeah, I can't remove/subtract that from "+key)
return "Can't remove that"
else:
while cmd[0] == ' ':
cmd = cmd[1:]
if type(data[key]) is dict:
new_key = cmd.split(" ")[0]
cmd = cmd[len(new_key):]
if cmd != "":
while cmd[0] == " ":
cmd = cmd[1:]
if cmd == "":
break
self.bot.log("Looking up "+new_key+" in "+key)
look_up_result = self.look_up(data[key],new_key,cmd)
if type(look_up_result) is dict:
data[key] = look_up_result
return data
else:
return look_up_result
elif type(data[key]) != list:
self.bot.log("Trying to change "+key+" to "+cmd)
try:
cmd = type(data[key])(cmd)
data[key] = cmd
self.bot.log("Did that")
return data
except:
self.bot.log("No. That did not work")
return "Wrong data type"
else:
self.bot.log("Yeah, that didn't work")
return "Wrong data type"
else:
self.bot.log("Couldn't find "+key)
self.bot.log("Testing to see if it's a multi-word key")
cmd = key + " " + cmd
words = cmd.split(" ")
search = ""
i = 0
while search not in data:
try:
search += " " + words[i]
i += 1
except:
self.bot.log("It wasn't. "+search+" doesn't exist")
return search + " doesn't exist"
if search[0] == " ":
search = search[1:]
self.bot.log("Yeah! Did that! The key was "+search)
cmd = cmd[len(search):]
if cmd != "":
while cmd[0] == " ":
cmd = cmd[1:]
if cmd == "":
break
if cmd == "":
self.bot.log("Returning "+search)
return self.set_up_dict(data[search])
else:
new_key = cmd.split(" ")[0]
cmd = cmd[len(new_key):]
if cmd != "":
while cmd[0] == " ":
cmd = cmd[1:]
if cmd == "":
break
look_up_result = self.look_up(data[search],new_key,cmd)
if type(look_up_result) is dict:
data[search] = look_up_result
return data
else:
return look_up_result
def character_sheet(self,character : dict):
self.bot.log("Setting up a character sheet for "+character["Name"])
divider = "--------------------\n"
name = character["Name"]
textf = ""
if character["Force-rating"] != 0:
textf = "\n**Force Rating**: "+str(character["Force-rating"])
text1 = "**Species**: "+character["Species"]+"\n**Career**: "+character["Career"]+"\n**Specialization Trees**: "+", ".join(character["Specialization-trees"])+textf+"\n**Soak**: "+str(character["Soak"])
text2 = "\n\n**Wounds**: "+str(character["Wounds"])+"/"+str(character["Wound-threshold"])+"\n**Strain**: "+str(character["Strain"])+"/"+str(character["Strain-threshold"])
text3 = self.set_up_dict(character["Characteristics"])
text4 = self.set_up_dict(character["Skills"])
text5 = ""
text6 = ""
text7 = ""
text8 = ""
if bool(character["Talents"]):
text5 = "**Talents**: "+", ".join(list(character["Talents"]))+"\n\n"
if bool(character["Force-powers"]):
text6 = "**Force Powers**: "+", ".join(list(character["Force-powers"]))+"\n\n"
text7 = "**Equipment**: "+", ".join(character["Equipment"])+"\n**Credits**: "+str(character["Credits"])+"\n**Weapons**: "+", ".join(list(character["Weapons"]))+"\n"+divider
if bool(character["Obligations"]):
text8 = "**Obligations**: "+",".join(list(character["Obligations"]))
return name, text1+text2+"\n\n"+text3+divider+text4+"\n"+divider+text5+text6+text7+text8
def char_data(self,user : str,cmd : str):
user_character = self.bot.database["starwars characters"].find_one({"_id":user})
key = string.capwords(cmd.split(" ")[0])
cmd = cmd[len(key):]
if cmd == " ":
cmd = ""
elif cmd != "":
while cmd[0] == " ":
cmd = cmd[1:]
if cmd == "":
break
self.bot.log("Looking for "+self.bot.database_funcs.get_name(user)+"'s character")
if user_character != None:
self.bot.log("Found it! Looking for "+key+" in the data")
if key in user_character:
self.bot.log("Found it!")
if type(user_character[key]) is dict:
self.bot.log("It's a dictionary!")
if cmd == "":
self.bot.log("Retrieving data")
if key == "Weapons":
if bool(user_character[key]):
self.bot.log("Returning a list of weapons")
return ", ".join(list(user_character[key]))
else:
self.bot.log("The character doesn't have any weapons. Which is probably for the best. Like, who just walks around with weapons?")
return "There doesn't seem to be anything there..."
else:
return self.set_up_dict(user_character[key])
elif cmd[0] == "+":
self.bot.log("Gonna add something!!!")
try:
cmd = cmd[1:]
while cmd[0] == ' ':
cmd = cmd[1:]
except:
self.bot.log("Nope. That didn't happen")
return "Can't do that"
if (key == "Talents" or key == "Force-powers") and "," in cmd:
cmd = cmd.split(",")
while cmd[1][0] == " ":
cmd[1] = cmd[1][1:]
self.bot.log("Adding "+cmd[0]+" to "+key)
self.bot.database["starwars characters"].update_one({"_id":user},
{"$set": {key+"."+cmd[0] : cmd[1]}})
return cmd[0]+" added to "+key+" for " + user_character["Name"]
elif key == "Obligations" and "," in cmd:
cmd = cmd.split(",")
while cmd[1][0] == " ":
cmd[1] = cmd[1][1:]
self.bot.log("Adding "+cmd[0]+" to "+key)
try:
self.bot.database["starwars characters"].update_one({"_id":user},
{"$set": {key+"."+cmd[0] : int(cmd[1])}})
except:
self.bot.log("Fucked that up")
return "Wrong data type"
return cmd[0]+" added to "+key+" for " + user_character["Name"]
elif key == "Weapons":
with open("gwendolyn/resources/star_wars/starwarstemplates.json", "r") as file_pointer:
templates = json.load(file_pointer)
new_weapon = templates["Weapon"]
self.bot.log("Adding "+cmd+" to "+key)
self.bot.database["starwars characters"].update_one({"_id":user},
{"$set": {key+"."+cmd : new_weapon}})
return cmd+" added to weapons for " + user_character["Name"]
else:
self.bot.log("That's not happening")
return "Can't add that"
elif cmd[0] == "-":
self.bot.log("Gonna subtract/remove something")
try:
cmd = cmd[1:]
while cmd[0] == ' ':
cmd = cmd[1:]
except:
self.bot.log("AAAAAAAAAAAA ")
return "Can't do that "
if key == "Talents" or key == "Force-powers" or key == "Weapons" or key == "Obligations":
self.bot.log("Trying to remove "+cmd+" from "+key)
if cmd in user_character[key]:
self.bot.database["starwars characters"].update_one({"_id":user},
{"$unset": {cmd}})
self.bot.log("I did that")
return cmd+" removed from "+key+" from "+user_character["Name"]
else:
self.bot.log("Welp. I fucked that up")
return "Can't remove that"
else:
self.bot.log("Urgh!")
return "Can't remove that"
else:
self.bot.log("Looking up "+cmd+" in "+key)
if key == "Talents" or key == "Force-powers":
new_key = cmd
new_cmd = ""
else:
new_key = string.capwords(cmd.split(" ")[0])
new_cmd = cmd[len(new_key):]
look_up_result = self.look_up(user_character[key],new_key,new_cmd)
if type(look_up_result) is dict:
self.bot.database["starwars characters"].update_one({"_id":user},
{"$set": {key : look_up_result}})
return "Changed " + user_character["Name"] + "'s " + key
else:
return look_up_result
else:
if cmd == "":
self.bot.log("Retrieving data")
if type(user_character[key]) is list:
return key+":\n"+", ".join(user_character[key])
else:
return user_character[key]
elif cmd[0] == '+':
self.bot.log("Adding")
try:
cmd = cmd[1:]
while cmd[0] == ' ':
cmd = cmd[1:]
except:
self.bot.log("Error message")
return "Can't do that"
if type(user_character[key]) is int:
try:
self.bot.log("Adding "+cmd+" to "+key)
self.bot.database["starwars characters"].update_one({"_id":user},
{"$inc": {key : int(cmd)}})
return "Added " + cmd + " to " + user_character["Name"] + "'s " + key
except:
self.bot.log("BITCH SANDWICH")
return "Can't add that"
elif type(user_character[key]) is list:
try:
self.bot.log("Adding "+cmd+" to "+key)
self.bot.database["starwars characters"].update_one({"_id":user},
{"$push": {key : cmd}})
return "Added " + cmd + " to " + user_character["Name"] + "'s " + key
except:
self.bot.log("tstststststs")
return "Can't add that"
else:
self.bot.log("Help")
return "Can't add that"
elif cmd[0] == '-':
self.bot.log("Removing/subtracting")
try:
cmd = cmd[1:]
while cmd[0] == ' ':
cmd = cmd[1:]
except:
self.bot.log("lalalala ")
return "Can't do that"
if type(user_character[key]) is int:
try:
self.bot.log("Subtracting "+cmd+" from "+key)
self.bot.database["starwars characters"].update_one({"_id":user},
{"$inc": {key : -int(cmd)}})
return "Subtracted " + cmd + " from " + user_character["Name"] + "'s " + key
except:
self.bot.log("Tried it. Didn't want to")
return "Can't remove that"
elif type(user_character[key]) is list:
try:
self.bot.log("removing "+cmd+" from "+key)
try:
self.bot.database["starwars characters"].update_one({"_id":user},
{"$pull": {key : cmd}})
except:
self.bot.log("They can only remove stuff that's actually in the list")
return "Not in list"
return "Removed " + cmd + " from " + user_character["Name"] + "'s " + key
except:
self.bot.log("nah")
return "Can't remove that"
else:
self.bot.log("nyope")
return "Can't remove that"
else:
self.bot.log("Changing "+key+" to "+cmd)
if type(user_character[key]) is int:
try:
self.bot.database["starwars characters"].update_one({"_id":user},
{"$set": {key : int(cmd)}})
except:
self.bot.log("I don't wanna tho")
return "Can't do that"
elif type(user_character[key]) is str:
self.bot.database["starwars characters"].update_one({"_id":user},
{"$set": {key : cmd}})
else:
self.bot.log("I don't wanna tho")
return "Can't do that"
return "Changed " + user_character["Name"] + "'s " + key +" to " + cmd
else:
self.bot.log(key+" isn't in there")
return "Couldn't find that data. Are you sure you spelled it correctly?"
else:
self.bot.log(user+" doesn't have a character")
return "You don't have a character. You can make one with /starwarscharacter"
def replace_spaces(self,cmd : str):
with_spaces = ["Specialization Trees","Wound Threshold","Strain Threshold","Defense - Ranged","Defense - Melee","Force Rating","Core Worlds","Outer Rim","Piloting - Planetary","Piloting - Space","Ranged - Heavy","Ranged - Light","Lightsaber Characteristic","Critical Injuries","Force Powers"]
without_spaces = ["Specialization-trees","Wound-threshold","Strain-threshold","Defense-ranged","Defense-melee","Force-rating","Core-worlds","Outer-rim","Piloting-planetary","Piloting-space","Ranged-heavy","Ranged-light","Lightsaber-characteristic","Critical-injuries","Force-powers"]
for i, value in enumerate(without_spaces):
cmd = cmd.replace(with_spaces[i],value)
return cmd
def replace_with_spaces(self,cmd : str):
with_spaces = ["Specialization Trees","Wound Threshold","Strain Threshold","Defense - Ranged","Defense - Melee","Force Rating","Core Worlds","Outer Rim","Piloting - Planetary","Piloting - Space","Ranged - Heavy","Ranged - light","Lightsaber Characteristic","Critical Injuries","Force Powers"]
without_spaces = ["Specialization-trees","Wound-threshold","Strain-threshold","Defense-ranged","Defense-melee","Force-rating","Core-worlds","Outer-rim","Piloting-planetary","Piloting-space","Ranged-heavy","Ranged-light","Lightsaber-characteristic","Critical-injuries","Force-powers"]
for i, value in enumerate(without_spaces):
cmd = cmd.replace(value,with_spaces[i])
return cmd
async def parse_char(self, ctx, parameters : str):
user = f"#{ctx.author.id}"
cmd = string.capwords(parameters.replace("+","+ ").replace("-","- ").replace(",",", "))
return_embed = False
cmd = self.replace_spaces(cmd)
user_character = self.bot.database["starwars characters"].find_one({"_id":user})
if cmd == " ":
cmd = ""
elif cmd != "":
while cmd[0] == " ":
cmd = cmd[1:]
if cmd == "":
break
if cmd == "":
if user_character != None:
title, text = self.character_sheet(user_character)
text = self.replace_with_spaces(text)
return_embed = True
else:
self.bot.log("Makin' a character for "+self.bot.database_funcs.get_name(user))
with open("gwendolyn/resources/star_wars/starwarstemplates.json", "r") as file_pointer:
templates = json.load(file_pointer)
new_char = templates["Character"]
new_char["_id"] = user
self.bot.database["starwars characters"].insert_one(new_char)
await ctx.send("Character for " + self.bot.database_funcs.get_name(user) + " created")
else:
if cmd == "Purge":
self.bot.log("Deleting "+self.bot.database_funcs.get_name(user)+"'s character")
self.bot.database["starwars characters"].delete_one({"_id":user})
await ctx.send("Character for " + self.bot.database_funcs.get_name(user) + " deleted")
else:
await ctx.send(self.replace_with_spaces(str(self.char_data(user,cmd))))
if return_embed:
embed = discord.Embed(title = title, description = text, colour=0xDEADBF)
await ctx.send(embed = embed)
def lightsaber_char(self,user : str):
user_character = self.bot.database["starwars characters"].find_one({"_id":user})
if user_character != None:
return user_character["Lightsaber-characteristic"]
def user_has_char(self,user : str):
user_character = self.bot.database["starwars characters"].find_one({"_id":user})
return user_character != None

View File

@@ -1,72 +0,0 @@
class StarWarsDestiny():
def __init__(self, bot):
self.bot = bot
def destiny_new(self, num : int):
self.bot.log("Creating a new destiny pool with "+str(num)+" players")
roll, dice_results = self.bot.star_wars.roll.roll(0,0,0,0,0,0,num)
roll = "".join(sorted(roll))
with open("gwendolyn/resources/star_wars/destinyPoints.txt","wt") as file_pointer:
file_pointer.write(roll)
return "Rolled for Destiny Points and got:\n"+self.bot.star_wars.roll.diceResultToEmoji(dice_results)+"\n"+self.bot.star_wars.roll.resultToEmoji(roll)
def destiny_use(self, user : str):
with open("gwendolyn/resources/star_wars/destinyPoints.txt","rt") as file_pointer:
points = file_pointer.read()
if user == "Nikolaj":
self.bot.log("Trying to use a dark side destiny point")
if 'B' in points:
points = points.replace("B","L",1)
points = "".join(sorted(points))
with open("gwendolyn/resources/star_wars/destinyPoints.txt","wt") as file_pointer:
file_pointer.write(points)
self.bot.log("Did it")
return "Used a dark side destiny point. Destiny pool is now:\n"+self.bot.star_wars.roll.resultToEmoji(points)
else:
self.bot.log("There were no dark side destiny points")
return "No dark side destiny points"
else:
self.bot.log("Trying to use a light side destiny point")
if 'L' in points:
points = points.replace("L","B",1)
points = "".join(sorted(points))
with open("gwendolyn/resources/star_wars/destinyPoints.txt","wt") as file_pointer:
file_pointer.write(points)
self.bot.log("Did it")
return "Used a light side destiny point. Destiny pool is now:\n"+self.bot.star_wars.roll.resultToEmoji(points)
else:
self.bot.log("There were no dark side destiny points")
return "No light side destiny points"
async def parse_destiny(self, ctx, cmd : str):
user = f"#{ctx.author.id}"
if cmd != "":
while cmd[0] == ' ':
cmd = cmd[1:]
if cmd == "":
break
if cmd == "":
self.bot.log("Retrieving destiny pool info")
with open("gwendolyn/resources/star_wars/destinyPoints.txt","rt") as file_pointer:
send_message = self.bot.star_wars.roll.resultToEmoji(file_pointer.read())
else:
commands = cmd.upper().split(" ")
if commands[0] == "N":
if len(commands) > 1:
send_message = self.destiny_new(int(commands[1]))
else:
send_message = "You need to give an amount of players"
elif commands[0] == "U":
send_message = self.destiny_use(user)
else:
send_message = "I didn't quite understand that"
message_list = send_message.split("\n")
await ctx.send(message_list[0])
if len(message_list) > 1:
for message_item in message_list[1:]:
await ctx.channel.send(message_item)

View File

@@ -1,391 +0,0 @@
import random
import re
import string
import json
with open("gwendolyn/resources/star_wars/starwarsskills.json", "r") as f:
skill_data = json.load(f)
class StarWarsRoll():
def __init__(self, bot):
self.bot = bot
# Rolls the specified dice
def roll(self, abi : int = 1, prof : int = 0, dif : int = 3, cha : int = 0, boo : int = 0, setb : int = 0, force : int = 0):
result = ""
dice_result = []
for _ in range(abi):
choice = random.choice(["","S","S","SS","A","A","SA","AA"])
result += choice
dice_result.append("abi"+choice)
for _ in range(prof):
choice = random.choice(["","S","S","SS","SS","A","SA","SA","SA","AA","AA","R"])
result += choice
dice_result.append("prof"+choice)
for _ in range(dif):
choice = random.choice(["","F","FF","H","H","H","HH","FH"])
result += choice
dice_result.append("dif"+choice)
for _ in range(cha):
choice = random.choice(["","F","F","FF","FF","H","H","FH","FH","HH","HH","D"])
result += choice
dice_result.append("cha"+choice)
for _ in range(boo):
choice = random.choice(["","","S","SA","AA","A"])
result += choice
dice_result.append("boo"+choice)
for _ in range(setb):
choice = random.choice(["","","F","F","H","H"])
result += choice
dice_result.append("setb"+choice)
for _ in range (force):
choice = random.choice(["B","B","B","B","B","B","BB","L","L","LL","LL","LL"])
result += choice
dice_result.append("force"+choice)
return result, dice_result
# Lets dice cancel each other out
def simplify(self, result : str):
self.bot.log("Simplifying "+result)
simp = ""
success = (result.count('S') + result.count('R')) - (result.count('F') + result.count('D'))
advantage = result.count('A') - result.count('H')
result = re.sub("S|A|F|H","",result)
if success > 0:
for _ in range(success):
simp += "S"
elif success < 0:
for _ in range(abs(success)):
simp += "F"
if advantage > 0:
for _ in range(advantage):
simp += "A"
elif advantage < 0:
for _ in range(abs(advantage)):
simp += "H"
simp += result
return simp
# Returns emoji that symbolize the dice results
def dice_result_to_emoji(self, dice_results : list):
emoji = ""
for result in dice_results:
if result == "abiA":
emoji += "<:abil1a:695267684476125264> "
elif result == "abiSA":
emoji += "<:abil1a1s:695267684484513842> "
elif result == "abiS":
emoji += "<:abil1s:695267684514005013> "
elif result == "abiAA":
emoji += "<:abil2a:695267684547428352> "
elif result == "abiSS":
emoji += "<:abil2s:695267684761206914> "
elif result == "abi":
emoji += "<:abilbla:695267684660674602> "
elif result == "profA":
emoji += "<:prof1a:695267685361123338> "
elif result == "profSA":
emoji += "<:prof1a1s:695267685067653140> "
elif result == "profR":
emoji += "<:prof1r:695267685067522088> "
elif result == "profS":
emoji += "<:prof1s:695267684899881012> "
elif result == "profAA":
emoji += "<:prof2a:695267684996218982> "
elif result == "profSS":
emoji += "<:prof2s:695267684878647327> "
elif result == "prof":
emoji += "<:profbla:695267684698292235> "
elif result == "difF":
emoji += "<:dif1f:695267684924915804> "
elif result == "difH":
emoji += "<:dif1h:695267684908138506> "
elif result == "difFH":
emoji += "<:dif1h1f:695267684908269678> "
elif result == "difFF":
emoji += "<:dif2f:695267684924784680> "
elif result == "difHH":
emoji += "<:dif2h:695267685071585340> "
elif result == "dif":
emoji += "<:difbla:695267685000544276> "
elif result == "chaD":
emoji += "<:cha1d:695267684962533447> "
elif result == "chaF":
emoji += "<:cha1f:695267684601954346> "
elif result == "chaH":
emoji += "<:cha1h:695267685046681620> "
elif result == "chaFH":
emoji += "<:cha1h1f:695267685063327784> "
elif result == "chaFF":
emoji += "<:cha2f:695267684832641097> "
elif result == "chaHH":
emoji += "<:cha2h:695267684631183381> "
elif result == "cha":
emoji += "<:chabla:695267684895686787> "
elif result == "booA":
emoji += "<:boo1a:695267684975116329> "
elif result == "booSA":
emoji += "<:boo1a1s:695267684970922024> "
elif result == "booS":
emoji += "<:boo1s:695267684979441714> "
elif result == "booAA":
emoji += "<:boo2a:695267685100945488> "
elif result == "boo":
emoji += "<:boobla:695267684757012550> "
elif result == "setbF":
emoji += "<:set1f:695267685054939197> "
elif result == "setbH":
emoji += "<:set1h:695267685147082802> "
elif result == "setb":
emoji += "<:setbla:695267685151408169> "
elif result == "forceB":
emoji += "<:for1b:695267684593434677> "
elif result == "forceL":
emoji += "<:for1l:695267684606148640> "
elif result == "forceBB":
emoji += "<:for2b:695267684903944303> "
elif result == "forceLL":
emoji += "<:for2l:695267684992024626> "
return emoji
# Returns emoji that symbolize the results of the dice rolls
def result_to_emoji(self, result : str):
emoji = ""
for char in result:
if char == 'S':
emoji += "<:success:826026925280854026> "
elif char == 'A':
emoji += "<:advantage:826026925515604009> "
elif char == 'R':
emoji += "<:triumph:826026925319127070> "
elif char == 'F':
emoji += "<:failure:826026925288980511> "
elif char == 'H':
emoji += "<:threat:826026925280985108> "
elif char == 'D':
emoji += "<:despair:826026925272203294> "
elif char == 'L':
emoji += "<:light:826026925059211295>"
elif char == 'B':
emoji += "<:dark:826026925289373717>"
return emoji
# Converts emoji into letters
def emoji_to_result(self, emoji : str):
result = ""
for char in emoji:
if char == "<:light:691010089905029171>":
emoji += 'L'
if char == "<:dark:691010101901000852>":
emoji += 'B'
return result
# Returns emoji that symbolize the dice
def dice_to_emoji(self, dice : list):
emoji = ""
for _ in range(dice[0]):
emoji += "<:ability:690974213397282826> "
for _ in range(dice[1]):
emoji += "<:proficiency:690973435354153071> "
for _ in range(dice[2]):
emoji += "<:difficulty:690973992470708296> "
for _ in range(dice[3]):
emoji += "<:challenge:690973419906400306> "
for _ in range(dice[4]):
emoji += "<:boost:690972178216386561> "
for _ in range(dice[5]):
emoji += "<:setback:690972157890658415> "
for _ in range(dice[6]):
emoji += "<:force:690973451883774013> "
return emoji
# Rolls for obligation
def obligation_roll(self):
self.bot.log("Rolling for obligation")
data = self.bot.database["starwarscharacters"]
table = []
for character in data:
for obligation in data[character]["Obligations"]:
for _ in range(data[character]["Obligations"][obligation]):
table.append(data[character]["Name"]+", "+obligation)
while len(table) < 100:
table.append("Nothing")
return random.choice(table)
# Rolls for critical injury
async def crit_roll(self, ctx, addington : int):
difficulty_die = "<:difficulty:690973992470708296>"
setback_die = "<:setback:690972157890658415>"
boost_die = "<:boost:690972178216386561>"
roll = random.randint(1,100) + addington
injuries = [
"**Minor nick**: The target suffers 1 strain, "+difficulty_die] * 5 + [
"**Slowed down**: The target can only act during the last allied initiative slot this turn, "+difficulty_die] * 5 + [
"**Sudden Jolt**: The target drops whatever is in hand, "+difficulty_die] * 5 + [
"**Distracted**: The target cannot perform a Free maneuver during his next turn, "+difficulty_die] * 5 + [
"**Off-Balance**: The target adds "+setback_die+" to his next skill check, "+difficulty_die] * 5 + [
"**Discouraging Wound**: Flip one light side Destiny point to a dark side Destiny point (reverse if NPC), "+difficulty_die] * 5 + [
"**Stunned**: The target is staggered until the end of his next turn, "+difficulty_die] * 5 + [
"**Stinger**: Increase the difficulty of next check by one, "+difficulty_die] * 5 + [
"**Bowled Over**: The target is knocked prone and suffers 1 strain, "+difficulty_die+difficulty_die] * 5 + [
"**Head Ringer**: The target increases the difficulty of all Intellect and Cunning checks by one until the end of the encounter, "+difficulty_die+difficulty_die] * 5 + [
"**Fearsome Wound**: The target increases the difficulty of all Presence and Willpower checks by one until the end of the encounter, "+difficulty_die+difficulty_die] * 5 + [
"**Agonizing Wound**: The target increases the difficulty of all Brawn and Agility checks by one until the end of the encounter, "+difficulty_die+difficulty_die] * 5 + [
"**Slightly Dazed**: The target is disoriented until the end of the encounter, "+difficulty_die+difficulty_die] * 5 + [
"**Scattered Senses**: The target removes all "+boost_die+" from skill checks until the end of the encounter, "+difficulty_die+difficulty_die] * 5 + [
"**Hamstrung**: The target loses his free maneuver until the end of the encounter, "+difficulty_die+difficulty_die] * 5 + [
"**Overpowered**: The target leaves himself open, and the attacker may immediately attempt another free attack agains him, using the exact same pool as the original, "+difficulty_die+difficulty_die] * 5 + [
"**Winded**: Until the end of the encounter, the target cannot voluntarily suffer strain to activate any abilities or gain additional maneuvers, "+difficulty_die+difficulty_die] * 5 + [
"**Compromised**: Incerase difficulty of all skill checks by one until the end of the encounter, "+difficulty_die+difficulty_die] * 5 + [
"**At the brink**: The target suffers 1 strain each time he performs an action, "+difficulty_die+difficulty_die+difficulty_die] * 5 + [
"**Crippled**: One of the target's limbs (selected by the GM) is crippled until healed or replaced. Increase difficulty of all checks that require use of that limb by one, "+difficulty_die+difficulty_die+difficulty_die] * 5 + [
"**Maimed**: One of the target's limbs (selected by the GM) is permanently lost. Unless the target has a cybernetic replacement, the target cannot perform actions that would require the use of that limb. All other actions gain "+setback_die+", "+difficulty_die+difficulty_die+difficulty_die] * 5 + [
"HI"] * 5 + [
"**Temporarily Lame**: Until this critical injury is healed, the target cannot perform more than one maneuver during his turn, "+difficulty_die+difficulty_die+difficulty_die] * 5 + [
"**Blinded**: The target can no longer see. Upgrade the difficulty of all checks twice. Upgrade the difficulty of perception checks three times, "+difficulty_die+difficulty_die+difficulty_die] * 5 + [
"**Knocked Senseless**: The target is staggered for the remainder of the encounter, "+difficulty_die+difficulty_die+difficulty_die] * 5 + [
"GI"] * 5 + [
"**Bleeding Out**: Every round, the target suffers 1 wound and 1 strain at the beginning of his turn. For every five wounds he suffers beyond his wound threshold, he suffers one additional critical injury. (If he suffers this one again, roll again), "+difficulty_die+difficulty_die+difficulty_die+difficulty_die] * 10 + [
"**The End is Nigh**: The target will die after the last initiative slot during the next round, "+difficulty_die+difficulty_die+difficulty_die+difficulty_die] * 10 + [
"**Dead**: U B Dead :("]
if roll >= len(injuries):
results = injuries[-1]
else:
results = injuries[roll]
if results == "HI":
characteristic = random.choice(["brawn"] * 3 + ["agility"] * 3 + ["intellect", "cunning", "presence"])
results = "**Horrific Injury**: Until this criticil injury is healed, treat the target's "+characteristic+" as if it's one lower, "+difficulty_die+difficulty_die+difficulty_die
if results == "GI":
characteristic = random.choice(["brawn"] * 3 + ["agility"] * 3 + ["intellect", "cunning", "presence"])
results = "**Gruesome Injury**: The target's "+characteristic+" is permanently one lower, "+difficulty_die+difficulty_die+difficulty_die+difficulty_die
send_message = "Roll: "+str(roll)+"\nInjury:\n"+results
message_list = send_message.split("\n")
await ctx.send(message_list[0])
if len(message_list) > 1:
for message_item in message_list[1:]:
await ctx.channel.send(message_item)
# Parses the command into something the other functions understand
async def parse_roll(self, ctx, cmd : str = ""):
user = f"#{ctx.author.id}"
cmd = re.sub(' +',' ',cmd.upper()) + " "
if cmd[0] == " ":
cmd = cmd[1:]
cmd = self.bot.star_wars.character.replaceSpaces(string.capwords(cmd))
commands = cmd.split(" ")
valid_command = False
if commands[0] == "":
roll_parameters = [1,0,3,0,0,0,0]
else:
roll_parameters = [0,0,0,0,0,0,0]
if string.capwords(commands[0]) == "Obligations":
send_message = self.obligation_roll()
elif string.capwords(commands[0]) in skill_data:
self.bot.log("Oh look! This guy has skills!")
if self.bot.star_wars.character.userHasChar(user):
self.bot.log("They have a character. That much we know")
skill_level = self.bot.star_wars.character.char_data(user,"Skills " + string.capwords(commands[0]))
if string.capwords(commands[0]) == "Lightsaber":
self.bot.log("The skill is lightsaber")
char_level = self.bot.star_wars.character.char_data(user,"Characteristics " + self.bot.star_wars.character.lightsaberChar(user))
else:
char_level = self.bot.star_wars.character.char_data(user,"Characteristics " + skill_data[string.capwords(commands[0])])
ability_dice = abs(char_level-skill_level)
proficiency_dice = min(skill_level,char_level)
commands = [str(ability_dice)] + [str(proficiency_dice)] + commands[1:]
self.bot.log("Converted skill to dice")
valid_command = True
else:
self.bot.log("Okay, no they don't i guess")
send_message = "You don't have a user. You can make one with /starwarscharacter"
elif string.capwords(commands[0]) in ["Ranged","Piloting"]:
self.bot.log("They fucked up writing the name of a ranged or piloting skill")
if string.capwords(commands[0]) == "Ranged":
send_message = "Did you mean \"Ranged - Heavy\" or \"Ranged - Light\""
else:
send_message = "Did you mean \"Piloting - Planetary\" or \"Piloting - Space\""
else:
valid_command = True
if valid_command:
self.bot.log("Converting commands to dice")
for i, command in enumerate(commands):
if command != "":
command = command.upper()
if command[0] == "A":
roll_parameters[0] = int(command.replace("A",""))
elif command[0] == "P":
roll_parameters[1] = int(command.replace("P",""))
elif command[0] == "D":
roll_parameters[2] = int(command.replace("D",""))
elif command[0] == "C":
roll_parameters[3] = int(command.replace("C",""))
elif command[0] == "B":
roll_parameters[4] = int(command.replace("B",""))
elif command[0] == "S":
roll_parameters[5] = int(command.replace("S",""))
elif command[0] == "F":
roll_parameters[6] = int(command.replace("F",""))
else:
roll_parameters[i] = int(command)
self.bot.log("Rolling "+str(roll_parameters))
roll_results, dice_results = self.roll(roll_parameters[0],roll_parameters[1],roll_parameters[2],roll_parameters[3],roll_parameters[4],roll_parameters[5],roll_parameters[6])
simplified = self.simplify(roll_results)
name = self.bot.star_wars.character.getChar_name(user)
self.bot.log("Returns results and simplified results")
if simplified == "":
send_message = name + " rolls: " + "\n" + self.dice_result_to_emoji(dice_results) + "\nEverything cancels out!"
else:
send_message = name + " rolls: " + "\n" + self.dice_result_to_emoji(dice_results) + "\n" + self.result_to_emoji(simplified)
message_list = send_message.split("\n")
await ctx.send(message_list[0])
if len(message_list) > 1:
for message_item in message_list[1:]:
if message_item == "":
self.bot.log("Tried to send empty message")
else:
await ctx.channel.send(message_item)

View File

@@ -1,135 +1,90 @@
""" from dotenv import load_dotenv
Contains the Gwendolyn class, a subclass of the discord command bot. from os import getenv, listdir
*Classes* from interactions import Client, Status
---------
Gwendolyn(discord.ext.commands.Bot)
"""
import os # Used for loading cogs in Gwendolyn.addCogs
import finnhub # Used to add a finhub client to the bot
import discord # Used for discord.Intents and discord.Status
import discord_slash # Used to initialized SlashCommands object
import git # Used to pull when stopping
from discord.ext import commands # Used to inherit from commands.bot
from pymongo import MongoClient # Used for database management from pymongo import MongoClient # Used for database management
from gwendolyn.funcs import Money, StarWars, Games, Other, LookupFuncs
from gwendolyn.utils import (get_options, get_credentials, log_this,
DatabaseFuncs, EventHandler, ErrorHandler,
long_strings)
from gwendolyn.utils import log
from gwendolyn.exceptions import NoToken, CannotConnectToService
from gwendolyn.funcs import Other, BetterNetflix, Sonarr, Radarr, TMDb, QBittorrent, Money, Games
class Gwendolyn(commands.Bot): class Gwendolyn(Client):
""" def __init__(self):
A multifunctional Discord bot. """Initialize the bot."""
initiation_parameters = {
"status": Status.DND,
"delete_unused_application_cmds": True,
"sync_interactions": True
}
super().__init__(**initiation_parameters)
*Methods* self._add_clients_and_options()
--------- self._add_functions()
log(messages: Union[str, list], channel: str = "", self._add_extensions()
level: int = 20)
stop(ctx: discord_slash.context.SlashContext)
defer(ctx: discord_slash.context.SlashContext)
"""
# pylint: disable=too-many-instance-attributes
def __init__(self): def _add_clients_and_options(self):
"""Initialize the bot.""" """Add all the client, option and credentials objects."""
intents = discord.Intents.default() load_dotenv()
intents.members = True self.testing = getenv("GWENDOLYN_TESTING", "False").lower() in ('true', '1', 't')
initiation_parameters = { if self.testing:
"command_prefix": " ", self.log("Testing mode", level=25)
"case_insensitive": True,
"intents": intents,
"status": discord.Status.dnd
}
super().__init__(**initiation_parameters)
self._add_clients_and_options() self.enable_nework_services = getenv("NETWORK_SERVICES", "False").lower() in ('true', '1', 't')
self._add_util_classes()
self._add_function_containers()
self._add_cogs()
def _add_clients_and_options(self): self.bot_token = getenv("DISCORD_TOKEN")
"""Add all the client, option and credentials objects."""
self.long_strings = long_strings()
self.options = get_options()
self.credentials = get_credentials()
finnhub_key = self.credentials["finnhub_key"]
self.finnhub_client = finnhub.Client(api_key=finnhub_key)
mongo_user = self.credentials["mongo_db_user"]
mongo_password = self.credentials["mongo_db_password"]
mongo_url = f"mongodb+srv://{mongo_user}:{mongo_password}@gwendolyn"
mongo_url += ".qkwfy.mongodb.net/Gwendolyn?retryWrites=true&w=majority"
database_clint = MongoClient(mongo_url)
if self.options["testing"]: if self.bot_token == "TOKEN":
self.log("Testing mode") raise NoToken()
self.database = database_clint["Gwendolyn-Test"]
else:
self.database = database_clint["Gwendolyn"]
def _add_util_classes(self): self.admins = getenv("ADMINS").split(",")
"""Add all the classes used as utility."""
self.database_funcs = DatabaseFuncs(self)
self.event_handler = EventHandler(self)
self.error_handler = ErrorHandler(self)
slash_parameters = {
"sync_commands": True,
"sync_on_cog_reload": True,
"override_type": True
}
self.slash = discord_slash.SlashCommand(self, **slash_parameters)
def _add_function_containers(self): mongo_user = getenv("MONGODB_USER")
"""Add all the function containers used for commands.""" mongo_password = getenv("MONGODB_PASSWORD")
self.star_wars = StarWars(self) mongo_url = f"mongodb+srv://{mongo_user}:{mongo_password}@gwendolyn"
self.other = Other(self) mongo_url += ".qkwfy.mongodb.net/Gwendolyn?retryWrites=true&w=majority"
self.lookup_funcs = LookupFuncs(self) database_client = MongoClient(mongo_url)
self.games = Games(self)
self.money = Money(self)
def _add_cogs(self): try:
"""Load cogs.""" database_client.admin.command("ping")
for filename in os.listdir("./gwendolyn/cogs"): self.log("Connected to Mango Client")
if filename.endswith(".py"): except:
self.load_extension(f"gwendolyn.cogs.{filename[:-3]}") raise CannotConnectToService("Mango Client")
def log(self, messages, channel: str = "", level: int = 20): # pylint:disable=no-self-use if self.testing:
"""Log a message. Described in utils/util_functions.py.""" self.log("Testing mode")
log_this(messages, channel, level) self.database = database_client["Gwendolyn-Test"]
else:
self.database = database_client["Gwendolyn"]
async def stop(self, ctx: discord_slash.context.SlashContext): def _add_functions(self):
""" self.other = Other(self)
Stop the bot, and stop running games. self.money = Money(self)
if self.enable_nework_services:
self.better_netflix = BetterNetflix(
self,
radarr=Radarr(getenv("RADARR_IP"),getenv("RADARR_PORT"),getenv("RADARR_API_KEY")),
sonarr=Sonarr(getenv("SONARR_IP"),getenv("SONARR_PORT"),getenv("SONARR_API_KEY")),
tmdb=TMDb(getenv("TMDB_API_ACCESS_TOKEN")),
qbittorrent=QBittorrent(
getenv("QBITTORRENT_IP"),
getenv("QBITTORRENT_PORT"),
getenv("QBITTORRENT_USERNAME"),
getenv("QBITTORRENT_PASSWORD")
)
)
Only stops the bot if the user in ctx.author is one of the def _add_extensions(self):
admins given in options.txt. """Load cogs."""
for filename in listdir("./gwendolyn/ext"):
if filename.endswith(".py"):
if filename == "better_netflix.py" and not self.enable_nework_services:
continue
*parameters* self.load_extension(f"gwendolyn.ext.{filename[:-3]}")
------------
ctx: discord_slash.context.SlashContext
The context of the "/stop" slash command.
"""
if f"#{ctx.author.id}" in self.options["admins"]:
await ctx.send("Pulling git repo and restarting...")
await self.change_presence(status=discord.Status.offline) def log(self, messages, channel: str = "", level: int = 20): # pylint:disable=no-self-use
"""Log a message. Described in utils/util_functions.py."""
log(messages, channel, level)
self.database_funcs.wipe_games() def start(self):
if not self.options["testing"]: super().start(self.bot_token)
git_client = git.cmd.Git("")
git_client.pull()
self.log("Logging out", level=25)
await self.close()
else:
log_message = f"{ctx.author.display_name} tried to stop me!"
self.log(log_message, str(ctx.channel_id))
await ctx.send(f"I don't think I will, {ctx.author.display_name}")
async def defer(self, ctx: discord_slash.context.SlashContext):
"""Send a "Gwendolyn is thinking" message to the user."""
try:
await ctx.defer()
except discord_slash.error.AlreadyResponded:
self.log("defer failed")

View File

@@ -1,3 +0,0 @@
`/invest` vil vise dig hvilke aktier du har. `/invest check [symbol]` viser dig en akties nuværende pris, hvor [symbol] er forkortelsen for firmaet. GwendoBucks er lig med 1 amerikans cent.
`/invest buy [symbol] [pris]` lader dig købe aktier. [pris] er mængden af GwendoBucks du bruger på at købe. Du kan købe for færre GwendoBucks end en enkelt akties pris, men ikke for mindre end 100 GwendoBucks.
`/invest buy [symbol] [pris]` lader dig sælge dine aktier. Du kan godt sælge for mindre end 100 GwendoBucks.

View File

@@ -1,27 +0,0 @@
{
"missing parameters" : "Missing command parameters. Try using `!help [command]` to find out how to use the command.",
"Can't log in": "Could not log in. Remember to write your bot token in the credentials.txt file",
"Blackjack all players standing": "All players are standing. The dealer now shows his cards and draws.",
"Blackjack first round": ". You can also double down with \"/blackjack double\" or split with \"/blackjack split\"",
"Blackjack commands": "You have 2 minutes to either hit or stand with \"/blackjack hit\" or \"/blackjack stand\"{}. It's assumed you're standing if you don't make a choice.",
"Blackjack double": "Adding another {} GwendoBucks to {}'s bet and drawing another card.",
"Blackjack different cards": "You can only split if your cards have the same value",
"Blackjack split": "Splitting {}'s hand into 2. Adding their original bet to the second hand. You can use \"/blackjack hit/stand/double 1\" and \"/blackjack hit/stand/double 2\" to play the different hands.",
"Blackjack started": "Blackjack game started. Use the buttons or \"/blackjack bet [amount]\" to enter the game within the next 30 seconds.",
"Blackjack going on": "There's already a blackjack game going on. Try again in a few minutes.",
"Stock value": "The current {} stock is valued at **{}** GwendoBucks",
"Stock parameters": "You must give both a stock name and an amount of GwendoBucks you wish to spend.",
"Trivia going on": "There's already a trivia question going on. Try again in like, a minute",
"Trivia time up": "Time's up! The answer was \"*{}) {}*\". Anyone who answered that has gotten 1 GwendoBuck",
"Connect 4 going on": "There's already a connect 4 game going on in this channel",
"Connect 4 placed": "{} placed a piece in column {}. It's now {}'s turn",
"Hangman going on": "There's already a Hangman game going on in the channel",
"Hangman lost game": " You've guessed wrong six times and have lost the game.",
"Hangman guessed word": " You've guessed the word! Congratulations! Adding 15 GwendoBucks to your account",
"Already on Plex": "{} is either already on Plex, downloading, or not available",
"No torrent": "{}No torrent exists. Likely because the movie is not yet released on DVD",
"No torrents downloading": "There are no torrents downloading right. If the torrent you're looking for was added more than 24 hours ago, it might already be on Plex.",
"Update": "{}\nThis message will update every 10 seconds for {} more minutes\n```",
"No updates": "{}\nThis message will not update anymore\n```",
"Invalid parameters": "Invalid or repeated parameters. Use '/help downloading' to see valid parameters."
}

View File

@@ -1,413 +1,100 @@
{ {
"add_movie" : { "misc": {
"name" : "add_movie", "hello" : {
"description" : "Request a movie for Plex", "name" : "hello",
"options" : [ "description" : "Greet Gwendolyn"
{ },
"name" : "movie", "help" : {
"description" : "The movie to request", "name" : "help",
"type" : 3, "description" : "Get help with a command",
"required" : "true" "options" : [
} {
] "name" : "command",
"description" : "The command you want help with",
"type" : 3,
"required" : "false"
}
]
},
"echo" : {
"name": "echo",
"description": "Make Gwendolyn repeat something",
"options" : [
{
"name" : "text",
"description" : "The text you want Gwendolyn to repeat",
"type" : 3,
"required" : "true"
}
]
},
"gen_name" : {
"name": "gen_name",
"description": "Generate a random name"
},
"ping" : {
"name" : "ping",
"description" : "Get Gwendolyn's latency to the server"
},
"roll" : {
"name": "roll",
"description": "Roll dice",
"options": [
{
"name": "dice",
"description": "The dice to be rolled",
"type": 3,
"required": "false"
}
]
},
"stop" : {
"name" : "stop",
"description" : "Stop Gwendolyn"
},
"thank" : {
"name" : "thank",
"description" : "Thank Gwendolyn for her service"
}
}, },
"add_show" : { "better_netflix": {
"name" : "add_show", "movie": {
"description" : "Request a show for Plex", "name": "movie",
"options" : [ "description": "Get a random movie from Better Netflix"
{ },
"name" : "show", "show": {
"description" : "The show to request", "name": "show",
"type" : 3, "description": "Get a random show from Better Netflix"
"required" : "true" },
} "downloading": {
] "name": "downloading",
"description": "Get a list of everything being downloaded"
}
}, },
"balance" : { "games": {
"name" : "balance", "money": {
"description" : "See your balance of GwendoBucks" "balance" : {
}, "name" : "balance",
"blackjack_bet" : { "description" : "See your balance of GwendoBucks"
"base" : "blackjack",
"name" : "bet",
"description" : "Enter the current blackjack game with a bet",
"options" : [
{
"name" : "bet",
"description" : "Your bet",
"type" : 4,
"required" : "true"
}
]
},
"blackjack_cards" : {
"base" : "blackjack",
"name" : "cards",
"description" : "Get a count of the cards used in blackjack games"
},
"blackjack_hilo" : {
"base" : "blackjack",
"name" : "hilo",
"description" : "Get the current hi-lo value for the cards used in blackjack games"
},
"blackjack_shuffle" : {
"base" : "blackjack",
"name" : "shuffle",
"description" : "Shuffle the cards used in blackjack games"
},
"blackjack_start" : {
"base" : "blackjack",
"name" : "start",
"description" : "Start a game of blackjack"
},
"connect_four_start_gwendolyn" : {
"base" : "connect_four",
"subcommand_group" : "start",
"name" : "Gwendolyn",
"description" : "Start a game of connect four against Gwendolyn",
"options" : [
{
"name" : "difficulty",
"description" : "The difficulty of Gwendolyn's AI",
"type" : 4,
"required" : "false"
}
]
},
"connect_four_start_user" : {
"base" : "connect_four",
"subcommand_group" : "start",
"name" : "user",
"description" : "Start a game of connect four against another user",
"options" : [
{
"name" : "user",
"description" : "The user to start a game against",
"type" : 6,
"required" : "true"
}
]
},
"downloading" : {
"name" : "downloading",
"description" : "See current downloads for Plex",
"options" : [
{
"name" : "parameters",
"description" : "Parameters for the command",
"type" : 3,
"required" : "false"
}
]
},
"game" : {
"name" : "game",
"description" : "Set the 'playing' text for Gwendolyn",
"options" : [
{
"name" : "game_text",
"description" : "The game to set the 'playing' text to",
"type" : 3,
"required" : "true"
}
]
},
"give" : {
"name" : "give",
"description" : "Give GwendoBucks to another user",
"options" : [
{
"name" : "user",
"description" : "The user you're sending GwendoBucks to",
"type" : 6,
"required" : "true"
}, },
{ "give" : {
"name" : "amount", "name" : "give",
"description" : "The number of GwendoBucks you're sending", "description" : "Give GwendoBucks to another user",
"type" : 4, "options" : [
"required" : "true"
}
]
},
"hangman" : {
"name" : "hangman",
"description" : "Start a game of hangman"
},
"hello" : {
"name" : "hello",
"description" : "Greet Gwendolyn"
},
"help" : {
"name" : "help",
"description" : "Get help with a command",
"options" : [
{
"name" : "command",
"description" : "The command you want help with",
"type" : 3,
"required" : "false"
}
]
},
"hex_place" : {
"base" : "hex",
"name" : "place",
"description" : "Place a piece on the hex board",
"options" : [
{
"name" : "coordinates",
"description" : "The coordinates to place the piece at",
"type" : 3,
"required" : "true"
}
]
},
"hex_start_gwendolyn" : {
"base" : "hex",
"subcommand_group" : "start",
"name" : "Gwendolyn",
"description" : "Start a game of hex against Gwendolyn",
"options" : [
{
"name" : "difficulty",
"description" : "The difficulty of Gwendolyn's AI",
"type" : 4,
"required" : "false"
}
]
},
"hex_start_user" : {
"base" : "hex",
"subcommand_group" : "start",
"name" : "user",
"description" : "Start a game of hex against another user",
"options" : [
{
"name" : "user",
"description" : "The user to start a game against",
"type" : 6,
"required" : "true"
}
]
},
"hex_surrender" : {
"base" : "hex",
"name" : "surrender",
"description" : "Surrender the game of hex"
},
"hex_swap" : {
"base" : "hex",
"name" : "swap",
"description" : "Perform a hex swap"
},
"hex_undo" : {
"base" : "hex",
"name" : "undo",
"description" : "Undo your last hex move"
},
"invest" : {
"name" : "invest",
"description" : "Invest GwendoBucks in the stock market",
"options" : [
{
"name" : "parameters",
"description" : "The parameters for the command",
"type" : 3,
"required" : "false"
}
]
},
"image" : {
"name" : "image",
"description" : "Get a random image from Bing"
},
"monster" : {
"name" : "monster",
"description" : "Look up a monster",
"options" : [
{
"name" : "query",
"description" : "The monster to look up",
"type" : 3,
"required" : "true"
}
]
},
"movie" : {
"name" : "movie",
"description" : "Get the name and information of a random movie"
},
"name" : {
"name" : "name",
"description" : "Generate a random name"
},
"ping" : {
"name" : "ping",
"description" : "Get the Gwendolyn's latency to the server"
},
"roll" : {
"name" : "roll",
"description" : "Roll rpg dice",
"options" : [
{
"name" : "dice",
"description" : "The dice to roll",
"type" : 3,
"required" : "false"
}
]
},
"spell" : {
"name" : "spell",
"description" : "Look up a spell",
"options" : [
{
"name" : "query",
"description" : "The spell to look up",
"type" : 3,
"required" : "true"
}
]
},
"star_wars_character" : {
"name" : "star_wars_character",
"description" : "Manage your Star Wars character sheet",
"options" : [
{
"name" : "parameters",
"description" : "The parameters for the command",
"type" : 3,
"required" : "false"
}
]
},
"star_wars_crit" : {
"name" : "star_wars_crit",
"description" : "Roll a Star Wars critical injury",
"options" : [
{
"name" : "severity",
"description" : "The severity of the hit",
"type" : 4,
"required" : "true"
}
]
},
"star_wars_destiny" : {
"name" : "star_wars_destiny",
"description" : "Use and see Star Wars Destiny points",
"options" : [
{
"name" : "parameters",
"description" : "The parameters for the command",
"type" : 3,
"required" : "false"
}
]
},
"star_wars_roll" : {
"name" : "star_wars_roll",
"description" : "Roll Star Wars dice",
"options" : [
{
"name" : "dice",
"description" : "The dice, or ability, to roll",
"type" : 3,
"required" : "false"
}
]
},
"stop" : {
"name" : "stop",
"description" : "Restart Gwendolyn"
},
"tavern" : {
"name" : "tavern",
"description" : "Generate a random tavern"
},
"thank" : {
"name" : "thank",
"description" : "Thank Gwendolyn for her service"
},
"trivia" : {
"name" : "trivia",
"description" : "Play a game of trivia",
"options" : [
{
"name" : "answer",
"description" : "Your answer to the trivia question",
"type" : 3,
"required" : "false",
"choices" : [
{ {
"name" : "a", "name" : "user",
"value" : "a" "description" : "The user you're sending GwendoBucks to",
"type" : 6,
"required" : "true"
}, },
{ {
"name" : "b", "name" : "amount",
"value" : "b" "description" : "The number of GwendoBucks you're sending",
}, "type" : 4,
{ "required" : "true"
"name" : "c",
"value" : "c"
},
{
"name" : "d",
"value" : "d"
} }
] ]
} }
] }
},
"wiki" : {
"name" : "wiki",
"description" : "Searches for and gets the info for a wiki page",
"options" : [
{
"name" : "page",
"description" : "The page to find",
"type" : 3,
"required" : "false"
}
]
},
"wolf" : {
"name" : "wolf",
"description" : "Performs a search on Wolfram Alpha",
"options" : [
{
"name" : "query",
"description" : "What to search for on Wolfram Alpha",
"type" : 3,
"required" : "true"
}
]
},
"wordle_start": {
"base": "wordle",
"name" : "start",
"description": "Start a game of wordle",
"options": [
{
"name": "letters",
"description" : "How many letters the word should be",
"type": 4,
"required": "false"
}
]
},
"wordle_guess": {
"base": "wordle",
"name" : "guess",
"description": "Guess a word in wordle",
"options": [
{
"name": "guess",
"description" : "Your guess",
"type": 3,
"required": "true"
}
]
} }
} }

View File

@@ -1,36 +0,0 @@
{
"Astrogation" : "Intellect",
"Computers" : "Intellect",
"Cool" : "Presence",
"Vigilance" : "Willpower",
"Mechanics" : "Intellect",
"Melee" : "Brawn",
"Perception" : "Cunning",
"Piloting-space" : "Agility",
"Ranged-heavy" : "Agility",
"Ranged-light" : "Agility",
"Athletics" : "Brawn",
"Coercion" : "Willpower",
"Coordination" : "Agility",
"Charm" : "Presence",
"Medicine" : "Intellect",
"Negotiation" : "Presence",
"Piloting-planetary" : "Agility",
"Stealth" : "Agility",
"skulduggery" : "Cunning",
"Brawl" : "Brawn",
"Discipline" : "Willpower",
"Gunnery" : "Agility",
"Core-worlds" : "Intellect",
"Outer-rim" : "Intellect",
"Underworld" : "Intellect",
"Leadership" : "Presence",
"Lore" : "Intellect",
"Resilience" : "Brawn",
"Streetwise" : "Cunning",
"Survival" : "Cunning",
"Xenology" : "Intellect",
"Lightsaber" : "Brawn",
"Education" : "Intellect",
"Deception" : "Cunning"
}

View File

@@ -1,82 +0,0 @@
{
"Character": {
"Name": "New Character",
"Species": "",
"Career": "",
"Specialization-trees": [],
"Soak": 0,
"Wound-threshold": 0,
"Wounds": 0,
"Strain-threshold": 0,
"Strain": 0,
"Defense-ranged": 0,
"Defense-melee": 0,
"Force-rating": 0,
"Characteristics": {
"Brawn": 2,
"Agility": 2,
"Intellect": 2,
"Cunning": 2,
"Willpower": 2,
"Presence": 2
},
"Skills": {
"Astrogation": 0,
"Athletics": 0,
"Brawl": 0,
"Charm": 0,
"Coercion": 0,
"Computers": 0,
"Cool": 0,
"Coordination": 0,
"Core-worlds": 0,
"Discipline": 0,
"Deception": 0,
"Education": 0,
"Gunnery": 0,
"Leadership": 0,
"Lightsaber": 0,
"Lore": 0,
"Mechanics": 0,
"Medicine": 0,
"Melee": 0,
"Negotiation": 0,
"Outer-rim": 0,
"Perception": 0,
"Piloting-planetary": 0,
"Piloting-space": 0,
"Ranged-heavy": 0,
"Ranged-light": 0,
"Resilience": 0,
"skulduggery": 0,
"Stealth": 0,
"Streetwise": 0,
"Survival": 0,
"Underworld": 0,
"Vigilance": 0,
"Xenology": 0
},
"Lightsaber-characteristic": "Brawn",
"Obligations": {},
"Morality": {
"Weakness": "",
"Strength": "",
"Conflict": "",
"Morality": ""
},
"Credits": 0,
"Equipment": [],
"Armor": "",
"Critical-injuries": {},
"Weapons": {},
"Talents": {},
"Force-powers": {}
},
"Weapon": {
"Skill" : "",
"Damage" : 0,
"Range" : "",
"Crit" : 0,
"Special" : []
}
}

View File

@@ -56,11 +56,8 @@
] ]
}, },
"txt": { "txt": {
"gwendolyn/resources/star_wars/destinyPoints.txt": "",
"gwendolyn/resources/movies.txt": "The Room",
"gwendolyn/resources/names.txt": "Gandalf\n", "gwendolyn/resources/names.txt": "Gandalf\n",
"credentials.txt" : "Bot token: TOKEN\nFinnhub API key: KEY\nWordnik API Key: KEY\nMongoDB user: USERNAME\nMongoDB password: PASSWORD\nWolframAlpha AppID: APPID\nRadarr API key: KEY\nSonarr API key: KEY\nqBittorrent username: USER\nqBittorrent password: PASSWORD", ".env" : "# Discord Bot Token\nDISCORD_TOKEN=TOKEN\n\n# Wordnik API Key\nWORDNIK_API_KEY=KEY\n\n# MongoDB Credentials\nMONGODB_USER=USERNAME\nMONGODB_PASSWORD=PASSWORD\n\n# WolframAlpha AppID\nWOLFRAM_APPID=APPID\n\n# Radarr API Key\nRADARR_API_KEY=KEY\n\n# Sonarr API Key\nSONARR_API_KEY=KEY\n\n# qBittorrent Credentials\nQBITTORRENT_USERNAME=USER\nQBITTORRENT_PASSWORD=PASSWORD\n\n# Admins (comma-separated list of Discord user IDs)\nADMINS="
"options.txt" : "Testing: True\nTesting guild ids:\nAdmins:"
}, },
"folder" : [ "folder" : [
"gwendolyn/resources/lookup", "gwendolyn/resources/lookup",
@@ -68,7 +65,6 @@
"gwendolyn/resources/games/connect_four_boards", "gwendolyn/resources/games/connect_four_boards",
"gwendolyn/resources/games/hex_boards", "gwendolyn/resources/games/hex_boards",
"gwendolyn/resources/games/hangman_boards", "gwendolyn/resources/games/hangman_boards",
"gwendolyn/resources/plex",
"gwendolyn/resources/games/old_images" "gwendolyn/resources/games/old_images"
] ]
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -1,12 +1,5 @@
"""A collections of utilities used by Gwendolyn and her functions.""" """A collections of utilities used by Gwendolyn and her functions."""
__all__ = ["get_options", "get_credentials", "DatabaseFuncs", "EventHandler", __all__ = ["make_files","log","PARAMS"]
"ErrorHandler", "get_params", "log_this", "cap", "make_files",
"replace_multiple", "emoji_to_command"]
from .helper_classes import DatabaseFuncs from .util_functions import make_files, log, PARAMS
from .event_handlers import EventHandler, ErrorHandler
from .util_functions import (get_params, log_this, cap, make_files,
replace_multiple, emoji_to_command, long_strings,
sanitize, get_options, get_credentials, encode_id,
decode_id)

View File

@@ -1,165 +0,0 @@
"""
Classes used to handle bot events and errors.
*Classes*
---------
EventHandler
ErrorHandler
"""
import traceback # Used to get the traceback of errors
import discord # Used to init discord.Game and discord.Status, as well
# as compare errors to discord errors and as typehints
from discord.ext import commands # Used to compare errors with command
# errors
from discord_slash.context import SlashContext, ComponentContext
from gwendolyn.utils.util_functions import decode_id
from gwendolyn.exceptions import InvalidInteraction
class EventHandler():
"""
Handles bot events.
*Methods*
---------
on_ready()
on_slash_command(ctx: discord_slash.context.SlashContext)
on_reaction_add(ctx: discord_slash.context.SlashContext)
"""
def __init__(self, bot):
"""Initialize the handler."""
self.bot = bot
async def on_ready(self):
"""Log and sets status when it logs in."""
await self.bot.database_funcs.imdb_commands()
name = self.bot.user.name
userid = str(self.bot.user.id)
logged_in_message = f"Logged in as {name}, {userid}"
self.bot.log(logged_in_message, level=25)
game = discord.Game("Use /help for commands")
online_status = discord.Status.online
await self.bot.change_presence(activity=game, status=online_status)
async def on_slash_command(self, ctx: SlashContext):
"""Log when a slash command is given."""
if ctx.subcommand_name is not None:
subcommand = f" {ctx.subcommand_name} "
else:
subcommand = " "
if ctx.subcommand_group is not None:
sub_command_group = f"{ctx.subcommand_group} "
else:
sub_command_group = ""
args = " ".join([str(i) for i in ctx.args])
full_command = f"/{ctx.command}{subcommand}{sub_command_group}{args}"
log_message = f"{ctx.author.display_name} ran {full_command}"
self.bot.log(log_message, str(ctx.channel_id), level=25)
async def on_component(self, ctx: ComponentContext):
"""Handle component interaction."""
info = decode_id(ctx.custom_id)
self.bot.log(f"Component action with info {info}")
channel = ctx.channel
author = str(ctx.author_id)
if info[0].lower() == "plex":
if info[1].lower() == "movie":
await self.bot.other.plex.add_movie(
ctx.origin_message,
info[2],
not isinstance(channel, discord.DMChannel)
)
return
elif info[1].lower() == "show":
await self.bot.other.plex.add_show(
ctx.origin_message,
info[2],
not isinstance(channel, discord.DMChannel)
)
else:
raise InvalidInteraction(ctx.custom_id, info)
elif info[0].lower() == "hangman" and author == info[2]:
if info[1].lower() == "guess":
await self.bot.games.hangman.guess(ctx, *info[3:])
elif info[1].lower() == "end":
await self.bot.games.hangman.stop(ctx, *info[3:])
else:
raise InvalidInteraction(ctx.custom_id, info)
elif info[0].lower() == "connectfour":
connect_four = self.bot.games.connect_four
if info[1].lower() == "place" and author == info[2]:
await connect_four.place_piece(
ctx,
info[3],
int(info[4]),
[int(info[5]), int(info[6])],
int(info[7]),
ctx.author_id,
int(info[8])
)
elif info[1].lower() == "end" and author in [info[2], info[3]]:
await connect_four.surrender(
ctx, [int(info[2]), int(info[3])], info[4], info[5]
)
else:
raise InvalidInteraction(ctx.custom_id, info)
elif info[0].lower() == "blackjack":
await self.bot.games.blackjack.decode_interaction(ctx, info[1:])
else:
raise InvalidInteraction(ctx.custom_id, info)
class ErrorHandler():
"""
Handles errors.
*Methods*
---------
on_slash_command_error(ctx: discord_slash.context.SlashContext,
error: Exception)
on_error(method: str)
"""
def __init__(self, bot):
"""Initialize the handler."""
self.bot = bot
async def on_slash_command_error(self, ctx: SlashContext,
error: Exception):
"""Log when there's a slash command."""
if isinstance(error, commands.CommandNotFound):
await ctx.send("That's not a command")
elif isinstance(error, discord.errors.NotFound):
self.bot.log("Deleted message before I could add all reactions")
elif isinstance(error, commands.errors.MissingRequiredArgument):
self.bot.log(f"{error}", str(ctx.channel_id))
await ctx.send(self.bot.long_strings["missing parameters"])
else:
params = [type(error), error, error.__traceback__]
exception = traceback.format_exception(*params)
exception_string = "".join(exception)
log_messages = [f"exception in /{ctx.name}", f"{exception_string}"]
self.bot.log(log_messages, str(ctx.channel_id), 40)
if isinstance(error, discord.errors.NotFound):
self.bot.log("Context is non-existant", level=40)
else:
await ctx.send("Something went wrong")
async def on_error(self, method: str):
"""Log when there's an error."""
exception = traceback.format_exc()
exception_string = "".join(exception)
log_messages = [f"exception in {method}", f"{exception_string}"]
self.bot.log(log_messages, level=40)

View File

@@ -1,173 +0,0 @@
"""
Contains classes used for utilities.
*Classes*
---------
DatabaseFuncs()
"""
import os # Used to test if files exist
import time # Used to test how long it's been since commands were synced
import re # Used in get_id
import discord # Used for type hints
class DatabaseFuncs():
"""
Manages database functions.
*Methods*
---------
get_name(user_id: str) -> str
get_id(user_name: str) -> str
delete_game(game_type: str, channel: str)
wipe_games()
connect_four_reaction_test(message: discord.Message,
user: discord.User) -> bool
imdb_commands()
"""
def __init__(self, bot):
"""Initialize the class."""
self.bot = bot
def get_name(self, user_id: str):
"""
Get the name of a user you have the # id of.
*Parameters:
------------
user_id: str
The id of the user you want the name of. The format is
"#" + str(discord.User.id)
*Returns*
---------
user_name: str
The name of the user. If the user couldn't be found,
returns the user_id.
"""
user = self.bot.database["users"].find_one({"_id": user_id})
if user_id == f"#{self.bot.user.id}":
return_name = "Gwendolyn"
elif user is not None:
return_name = user["user name"]
else:
self.bot.log(f"Couldn't find user {user_id}")
return_name = user_id
return return_name
def get_id(self, user_name: str):
"""
Get the id of a user you have the username of.
*Parameters:
------------
user_name: str
The name of the user you want the id of.
*Returns*
---------
user_id: str
The id of the user in the format "#" +
str(discord.User.id). If the user couldn't be found,
returns the user_name.
"""
user_search = {"user name": re.compile(user_name, re.IGNORECASE)}
user = self.bot.database["users"].find_one(user_search)
if user is not None:
return_id = user["_id"]
else:
self.bot.log("Couldn't find user "+user_name)
return_id = None
return return_id
def delete_game(self, game_type: str, channel: str):
"""
Remove a game from the database.
*Parameters*
------------
game_type: str
The name of the collection the game is in, like
"hangman games", "blackjack games" etc.
channel: str
The channel id of the channel the game is on as a
string.
"""
self.bot.database[game_type].delete_one({"_id": channel})
def wipe_games(self):
"""Delete all running games and pull from git."""
game_types = [
"trivia questions",
"blackjack games",
"connect 4 games"
"hex games",
"wordle games"
]
for game_type in game_types:
self.bot.database[game_type].delete_many({})
def connect_four_reaction_test(self, message: discord.Message,
user: discord.User):
"""
Test if the given message is the current connect four game.
Also tests if the given user is the one who's turn it is.
*Parameters*
------------
message: discord.Message
The message to test.
user: discord.User
The user to test.
*Returns*
---------
: bool
Whether the given message is the current connect four
game and if the user who reacted is the user who's turn
it is.
"""
channel = message.channel
channel_search = {"_id": str(channel.id)}
game = self.bot.database["connect 4 games"].find_one(channel_search)
old_images_path = "gwendolyn/resources/games/old_images/"
file_path = old_images_path + f"connect_four{channel.id}"
if os.path.isfile(file_path):
with open(file_path, "r", encoding="utf-8") as file_pointer:
old_image = int(file_pointer.read())
else:
old_image = 0
if message.id == old_image:
self.bot.log("They reacted to the connect_four game")
turn = game["turn"]
if user == game["players"][turn]:
valid_reaction = True
else:
self.bot.log("It wasn't their turn")
valid_reaction = False
else:
valid_reaction = False
return valid_reaction
async def imdb_commands(self):
"""Sync the slash commands with the discord API."""
collection = self.bot.database["last synced"]
last_synced = collection.find_one()
now = time.time()
if last_synced["last synced"] < now - 86400:
slash_command_list = await self.bot.slash.to_dict()
self.bot.log(f"Updating commands: {slash_command_list}")
await self.bot.slash.sync_all_commands()
id_number = last_synced["_id"]
query_filter = {"_id": id_number}
update = {"$set": {"last synced": now}}
collection.update_one(query_filter, update)

View File

@@ -1,37 +1,7 @@
"""
Contains utility functions used by parts of the bot.
*Functions*
-----------
sanitize(data: str, lower_case_value: bool = false) -> dict
get_options() -> dict
get_credentials() -> dict
long_strings() -> dict
get_params() -> dict
log_this(messages: Union[str, list], channel: str = "",
level: int = 20)
cap(s: str) -> str
make_files()
replace_multiple(main_string: str, to_be_replaced: list,
new_string: str) -> str
emoji_to_command(emoji: str) -> str
"""
import string
import json # Used by long_strings(), get_params() and make_files() import json # Used by long_strings(), get_params() and make_files()
import logging # Used for logging import logging # Used for logging
import os # Used by make_files() to check if files exist import os # Used by make_files() to check if files exist
import sys # Used to specify printing for logging import sys # Used to specify printing for logging
import imdb # Used to disable logging for the module
BASE_37 = ":" + string.digits + string.ascii_uppercase
BASE_128 = list(
string.digits +
string.ascii_letters +
"!#$€£¢¥¤&%()*+,-./;:<=>?@[]_{|}~ `¦§©®«»±µ·¿əʒ" +
"ÆØÅÐÉÈÊÇÑÖ" +
"æøåðéèêçñö"
)
# All of this is logging configuration # All of this is logging configuration
FORMAT = " %(asctime)s | %(name)-16s | %(levelname)-8s | %(message)s" FORMAT = " %(asctime)s | %(name)-16s | %(levelname)-8s | %(message)s"
@@ -53,115 +23,6 @@ handler.setFormatter(logging.Formatter(fmt=PRINTFORMAT, datefmt=DATEFORMAT))
printer.addHandler(handler) printer.addHandler(handler)
printer.propagate = False printer.propagate = False
imdb._logging.setLevel("CRITICAL") # pylint: disable=protected-access
# Basically disables imdbpy logging, since it's being printed to the
# terminal.
def sanitize(data: str, lower_case_value: bool = False):
"""
Sanitize and create a dictionary from a string.
Each element is created from a line with a : in it. The key is left
of the :, the value is right of it.
*Parameters*
------------
data: str
The string to create a dict from.
lower_case_value: bool = False
Whether the value of each element should be lowercase.
*Returns*
---------
dct: dict
The sanitized dictionary of elements.
"""
data = data.splitlines()
dct = {}
for line in data:
if line[0] != "#" and ":" in line:
line_values = line.split(":")
line_values[0] = line_values[0].lower()
line_values[1] = line_values[1].replace(" ", "")
if lower_case_value:
line_values[1] = line_values[1].lower()
if line_values[0] in ["testing guild ids", "admins"]:
line_values[1] = line_values[1].split(",")
if all(i.isnumeric() for i in line_values[1]):
line_values[1] = [int(i) for i in line_values[1]]
if any(i == line_values[1] for i in ["true", "false"]):
line_values[1] = (line_values[1] == "true")
dct[line_values[0]] = line_values[1]
return dct
def get_options():
"""
Get the bot options as dict.
*Returns*
---------
options: dict
The options of the bot.
"""
with open("options.txt", "r", encoding="utf-8") as file_pointer:
data = sanitize(file_pointer.read(), True)
options = {}
options["testing"] = data["testing"]
options["guild_ids"] = data["testing guild ids"]
options["admins"] = data["admins"]
return options
def get_credentials():
"""
Returns the credentials used by the bot as a dict.
*Returns*
---------
credentials: dict
The credentials used by the bot.
"""
with open("credentials.txt", "r", encoding="utf-8") as file_pointer:
data = sanitize(file_pointer.read())
credentials = {}
credentials["token"] = data["bot token"]
credentials["finnhub_key"] = data["finnhub api key"]
credentials["wordnik_key"] = data["wordnik api key"]
credentials["mongo_db_user"] = data["mongodb user"]
credentials["mongo_db_password"] = data["mongodb password"]
credentials["wolfram_alpha_key"] = data["wolframalpha appid"]
credentials["radarr_key"] = data["radarr api key"]
credentials["sonarr_key"] = data["sonarr api key"]
credentials["qbittorrent_username"] = data["qbittorrent username"]
credentials["qbittorrent_password"] = data["qbittorrent password"]
return credentials
def long_strings():
"""
Get the data from gwendolyn/resources/long_strings.json.
*Returns*
---------
data: dict
The long strings and their keys.
"""
long_strings_path = "gwendolyn/resources/long_strings.json"
with open(long_strings_path, "r", encoding="utf-8") as file_pointer:
data = json.load(file_pointer)
return data
def get_params(): def get_params():
""" """
Get the slash command parameters. Get the slash command parameters.
@@ -175,16 +36,11 @@ def get_params():
with open(path, "r", encoding="utf-8") as file_pointer: with open(path, "r", encoding="utf-8") as file_pointer:
slash_parameters = json.load(file_pointer) slash_parameters = json.load(file_pointer)
options = get_options()
if options["testing"]:
for parameter in slash_parameters:
slash_parameters[parameter]["guild_ids"] = options["guild_ids"]
return slash_parameters return slash_parameters
PARAMS = get_params()
def log_this(messages, channel: str = "", level: int = 20): def log(messages, channel: str = "", level: int = 20):
""" """
Log something in Gwendolyn's logs. Log something in Gwendolyn's logs.
@@ -222,49 +78,19 @@ def log_this(messages, channel: str = "", level: int = 20):
for log_message in messages: for log_message in messages:
logger.log(level, log_message) logger.log(level, log_message)
def cap(input_string: str):
"""
Capitalize a string like a movie title.
That means "of" and "the" are not capitalized.
*Parameters*
------------
input_string: str
The string to capitalized.
*Returns*
---------
return_string: str
The capitalized string.
"""
no_caps_list = ["of", "the"]
word_number = 0
string_list = input_string.split()
return_string = ''
for word in string_list:
word_number += 1
if word not in no_caps_list or word_number == 1:
word = word.capitalize()
return_string += word+" "
return_string = return_string[:-1]
return return_string
def make_files(): def make_files():
"""Create all the files and directories needed by Gwendolyn.""" """Create all the files and directories needed by Gwendolyn."""
def make_json_file(path, content): def make_json_file(path, content):
"""Create json file if it doesn't exist.""" """Create json file if it doesn't exist."""
if not os.path.isfile(path): if not os.path.isfile(path):
log_this(path.split("/")[-1]+" didn't exist. Making it now.") log(path.split("/")[-1]+" didn't exist. Making it now.")
with open(path, "w", encoding="utf-8") as file_pointer: with open(path, "w", encoding="utf-8") as file_pointer:
json.dump(content, file_pointer, indent=4) json.dump(content, file_pointer, indent=4)
def make_txt_file(path, content): def make_txt_file(path, content):
"""Create txt file if it doesn't exist.""" """Create txt file if it doesn't exist."""
if not os.path.isfile(path): if not os.path.isfile(path):
log_this(path.split("/")[-1]+" didn't exist. Making it now.") log(path.split("/")[-1]+" didn't exist. Making it now.")
with open(path, "w", encoding="utf-8") as file_pointer: with open(path, "w", encoding="utf-8") as file_pointer:
file_pointer.write(content) file_pointer.write(content)
@@ -272,121 +98,18 @@ def make_files():
"""Create directory if it doesn't exist.""" """Create directory if it doesn't exist."""
if not os.path.isdir(path): if not os.path.isdir(path):
os.makedirs(path) os.makedirs(path)
log_this("The "+path.split("/")[-1]+" directory didn't exist") log("The "+path.split("/")[-1]+" directory didn't exist")
file_path = "gwendolyn/resources/starting_files.json" file_path = "gwendolyn/resources/starting_files.json"
with open(file_path, "r", encoding="utf-8") as file_pointer: with open(file_path, "r", encoding="utf-8") as file_pointer:
data = json.load(file_pointer) data = json.load(file_pointer)
for path in data["folder"]:
directory(path)
for path, content in data["json"].items(): for path, content in data["json"].items():
make_json_file(path, content) make_json_file(path, content)
for path, content in data["txt"].items(): for path, content in data["txt"].items():
make_txt_file(path, content) make_txt_file(path, content)
for path in data["folder"]:
directory(path)
def replace_multiple(main_string: str, to_be_replaced: list, new_string: str):
"""
Replace multiple substrings in a string with the same substring.
*Parameters*
------------
main_string: str
The string to replace substrings in.
to_be_replaced: list
The substrings to replace.
new_string: str
The string to replace the substrings with.
*Returns*
---------
main_string: str
The string with the substrings replaced.
"""
# Iterate over the strings to be replaced
for elem in to_be_replaced:
# Check if string is in the main string
if elem in main_string:
# Replace the string
main_string = main_string.replace(elem, new_string)
return main_string
def emoji_to_command(emoji: str):
"""
Convert emoji to text.
*Parameters*
------------
emoji: str
The emoji to decipher.
*Returns*
---------
: str
The deciphered string.
"""
if emoji == "1":
return_value = 1
elif emoji == "2":
return_value = 2
elif emoji == "3":
return_value = 3
elif emoji == "4":
return_value = 4
elif emoji == "5":
return_value = 5
elif emoji == "6":
return_value = 6
elif emoji == "7":
return_value = 7
elif emoji == "🎲":
return_value = "roll"
elif emoji == "":
return_value = "none"
elif emoji == "✔️":
return_value = 1
else:
return_value = ""
return return_value
def encode_id(info: list):
letters = list(":".join(info))
dec = 0
for i, letter in enumerate(letters):
try:
dec += (37**i) * BASE_37.index(letter.upper())
except ValueError:
log_this(f"Could not encode letter {letter}", level=30)
custom_id = []
while dec:
custom_id.append(BASE_128[dec % 128])
dec = dec // 128
custom_id = ''.join(custom_id)
log_this(f"Encoded {info} to {custom_id}")
return custom_id
def decode_id(custom_id: str):
letters = list(custom_id)
dec = 0
for i, letter in enumerate(letters):
dec += (128**i) * BASE_128.index(letter)
info_string = []
while dec:
info_string.append(BASE_37[dec % 37])
dec = dec // 37
info = ''.join(info_string).split(':')
log_this(f"Decoded {custom_id} to be {info}")
return ''.join(info_string).split(':')

View File

@@ -0,0 +1,6 @@
"""The main module for Gwendolyn."""
# pylint: disable=invalid-name
__all__ = ["funcs", "utils", "Gwendolyn"]
from .gwendolyn_client import Gwendolyn

View File

@@ -0,0 +1,40 @@
"""Contains the EventCog, which runs code for specific bot events."""
from discord.ext import commands # Has the cog class
class EventCog(commands.Cog):
"""Handles bot events."""
def __init__(self, bot):
"""Initialize the bot."""
self.bot = bot
self.bot.on_error = self.on_error
@commands.Cog.listener()
async def on_ready(self):
"""Log and set bot status when bot logs in."""
await self.bot.event_handler.on_ready()
@commands.Cog.listener()
async def on_slash_command(self, ctx):
"""Log when a slash command is run."""
await self.bot.event_handler.on_slash_command(ctx)
@commands.Cog.listener()
async def on_slash_command_error(self, ctx, error):
"""Log when a slash error occurs."""
await self.bot.error_handler.on_slash_command_error(ctx, error)
async def on_error(self, method, *args, **kwargs): # pylint: disable=unused-argument
"""Log when an error occurs."""
await self.bot.error_handler.on_error(method)
@commands.Cog.listener()
async def on_component(self, ctx):
"""Handle when someone reacts to a message."""
await self.bot.event_handler.on_component(ctx)
def setup(bot):
"""Add the eventcog to the bot."""
bot.add_cog(EventCog(bot))

View File

@@ -0,0 +1,160 @@
"""Contains all the cogs that deal with game commands."""
from discord.ext import commands # Has the cog class
from interactions import cog_ext # Used for slash commands
from gwendolyn_old.utils import get_params # pylint: disable=import-error
params = get_params()
class GamesCog(commands.Cog):
"""Contains miscellaneous game commands."""
def __init__(self, bot):
"""Initialize the cog."""
self.bot = bot
@cog_ext.cog_slash(**params["balance"])
async def balance(self, ctx):
"""Check user balance."""
await self.bot.money.sendBalance(ctx)
@cog_ext.cog_slash(**params["give"])
async def give(self, ctx, user, amount):
"""Give another user an amount of GwendoBucks."""
await self.bot.money.giveMoney(ctx, user, amount)
@cog_ext.cog_slash(**params["trivia"])
async def trivia(self, ctx, answer=""):
"""Run a game of trivia."""
await self.bot.games.trivia.triviaParse(ctx, answer)
class BlackjackCog(commands.Cog):
"""Contains the blackjack commands."""
def __init__(self, bot):
"""Initialize the cog."""
self.bot = bot
@cog_ext.cog_subcommand(**params["blackjack_start"])
async def blackjack_start(self, ctx):
"""Start a game of blackjack."""
await self.bot.games.blackjack.start(ctx)
@cog_ext.cog_subcommand(**params["blackjack_bet"])
async def blackjack_bet(self, ctx, bet):
"""Enter the game of blackjack with a bet."""
await self.bot.games.blackjack.enter_game(ctx, bet)
@cog_ext.cog_subcommand(**params["blackjack_hilo"])
async def blackjack_hilo(self, ctx):
"""Get the hilo value for the deck in blackjack."""
await self.bot.games.blackjack.hilo(ctx)
@cog_ext.cog_subcommand(**params["blackjack_shuffle"])
async def blackjack_shuffle(self, ctx):
"""Shuffle the blackjack game."""
await self.bot.games.blackjack.shuffle(ctx)
@cog_ext.cog_subcommand(**params["blackjack_cards"])
async def blackjack_cards(self, ctx):
"""Get the amount of cards left in the blackjack deck."""
await self.bot.games.blackjack.cards(ctx)
class ConnectFourCog(commands.Cog):
"""Contains all the connect four commands."""
def __init__(self, bot):
"""Initialize the cog."""
self.bot = bot
@cog_ext.cog_subcommand(**params["connect_four_start_user"])
async def connect_four_start_user(self, ctx, user):
"""Start a game of connect four against another user."""
await self.bot.games.connect_four.start(ctx, user)
@cog_ext.cog_subcommand(**params["connect_four_start_gwendolyn"])
async def connect_four_start_gwendolyn(self, ctx, difficulty=3):
"""Start a game of connect four against Gwendolyn."""
await self.bot.games.connect_four.start(ctx, difficulty)
class HangmanCog(commands.Cog):
"""Contains all the hangman commands."""
def __init__(self, bot):
"""Initialize the cog."""
self.bot = bot
@cog_ext.cog_slash(**params["hangman"])
async def hangman(self, ctx):
"""Start a game of hangman."""
await self.bot.games.hangman.start(ctx)
class WordleCog(commands.Cog):
"""Contains all the wordle commands."""
def __init__(self, bot):
"""Initialize the cog."""
self.bot = bot
@cog_ext.cog_subcommand(**params["wordle_start"])
async def wordle_start(self, ctx, letters = 5):
"""Start a game of wordle."""
await self.bot.games.wordle.start(ctx, letters)
@cog_ext.cog_subcommand(**params["wordle_guess"])
async def wordle_guess(self, ctx, guess):
"""Start a game of wordle."""
await self.bot.games.wordle.guess(ctx, guess)
class HexCog(commands.Cog):
"""Contains all the hex commands."""
def __init__(self, bot):
"""Initialize the cog."""
self.bot = bot
self.hex = self.bot.games.hex
@cog_ext.cog_subcommand(**params["hex_start_user"])
async def hex_start_user(self, ctx, user):
"""Start a game of hex against another player."""
await self.hex.start(ctx, user)
@cog_ext.cog_subcommand(**params["hex_start_gwendolyn"])
async def hex_start_gwendolyn(self, ctx, difficulty=2):
"""Start a game of hex against Gwendolyn."""
await self.hex.start(ctx, difficulty)
@cog_ext.cog_subcommand(**params["hex_place"])
async def hex_place(self, ctx, coordinates):
"""Place a piece in the hex game."""
await self.hex.placeHex(ctx, coordinates, f"#{ctx.author.id}")
@cog_ext.cog_subcommand(**params["hex_undo"])
async def hex_undo(self, ctx):
"""Undo your last hex move."""
await self.hex.undo(ctx)
@cog_ext.cog_subcommand(**params["hex_swap"])
async def hex_swap(self, ctx):
"""Perform a hex swap."""
await self.hex.swap(ctx)
@cog_ext.cog_subcommand(**params["hex_surrender"])
async def hex_surrender(self, ctx):
"""Surrender the hex game."""
await self.hex.surrender(ctx)
def setup(bot):
"""Add all the cogs to the bot."""
bot.add_cog(GamesCog(bot))
bot.add_cog(BlackjackCog(bot))
bot.add_cog(ConnectFourCog(bot))
bot.add_cog(HangmanCog(bot))
bot.add_cog(HexCog(bot))
bot.add_cog(WordleCog(bot))

View File

@@ -0,0 +1,32 @@
"""Contains the LookupCog, which deals with the lookup commands."""
from discord.ext import commands # Has the cog class
from interactions import cog_ext # Used for slash commands
from gwendolyn_old.utils import get_params # pylint: disable=import-error
params = get_params()
class LookupCog(commands.Cog):
"""Contains the lookup commands."""
def __init__(self, bot):
"""Initialize the cog."""
self.bot = bot
# Looks up a spell
@cog_ext.cog_slash(**params["spell"])
async def spell(self, ctx, query):
"""Look up a spell."""
await self.bot.lookup_funcs.spell_func(ctx, query)
# Looks up a monster
@cog_ext.cog_slash(**params["monster"])
async def monster(self, ctx, query):
"""Look up a monster."""
await self.bot.lookup_funcs.monster_func(ctx, query)
def setup(bot):
"""Add the cog to the bot."""
bot.add_cog(LookupCog(bot))

View File

@@ -0,0 +1,100 @@
"""Contains the MiscCog, which deals with miscellaneous commands."""
from discord.ext import commands # Has the cog class
from interactions import cog_ext # Used for slash commands
from interactions import SlashContext
from gwendolyn_old.utils import get_params # pylint: disable=import-error
params = get_params()
class MiscCog(commands.Cog):
"""Contains the miscellaneous commands."""
def __init__(self, bot):
"""Initialize the cog."""
self.bot = bot
self.bot.remove_command("help")
self.generators = bot.other.generators
self.plex = bot.other.plex
self.nerd_shit = bot.other.nerd_shit
@cog_ext.cog_slash(**params["ping"])
async def ping(self, ctx: SlashContext):
"""Send the bot's latency."""
await ctx.send(f"Pong!\nLatency is {round(self.bot.latency * 1000)}ms")
@cog_ext.cog_slash(**params["stop"])
async def stop(self, ctx: SlashContext):
"""Stop the bot."""
await self.bot.stop(ctx)
@cog_ext.cog_slash(**params["help"])
async def help_command(self, ctx: SlashContext, command=""):
"""Get help for commands."""
await self.bot.other.help_func(ctx, command)
@cog_ext.cog_slash(**params["thank"])
async def thank(self, ctx: SlashContext):
"""Thank the bot."""
await ctx.send("You're welcome :blush:")
@cog_ext.cog_slash(**params["hello"])
async def hello(self, ctx: SlashContext):
"""Greet the bot."""
await self.bot.other.hello_func(ctx)
@cog_ext.cog_slash(**params["roll"])
async def roll(self, ctx: SlashContext, dice="1d20"):
"""Roll dice."""
await self.bot.other.roll_dice(ctx, dice)
@cog_ext.cog_slash(**params["image"])
async def image(self, ctx: SlashContext):
"""Get a random image from Bing."""
await self.bot.other.image_func(ctx)
@cog_ext.cog_slash(**params["movie"])
async def movie(self, ctx: SlashContext):
"""Get a random movie from the Plex server."""
await self.bot.other.movie_func(ctx)
@cog_ext.cog_slash(**params["name"])
async def name(self, ctx: SlashContext):
"""Generate a random name."""
await self.generators.name_gen(ctx)
@cog_ext.cog_slash(**params["tavern"])
async def tavern(self, ctx: SlashContext):
"""Generate a random tavern name."""
await self.generators.tavern_gen(ctx)
@cog_ext.cog_slash(**params["wiki"])
async def wiki(self, ctx: SlashContext, wiki_page=""):
"""Get a page on a fandom wiki."""
await self.bot.other.find_wiki_page(ctx, wiki_page)
@cog_ext.cog_slash(**params["add_movie"])
async def add_movie(self, ctx: SlashContext, movie):
"""Search for a movie and add it to the Plex server."""
await self.plex.request_movie(ctx, movie)
@cog_ext.cog_slash(**params["add_show"])
async def add_show(self, ctx: SlashContext, show):
"""Search for a show and add it to the Plex server."""
await self.plex.request_show(ctx, show)
@cog_ext.cog_slash(**params["downloading"])
async def downloading(self, ctx: SlashContext, parameters="-d"):
"""Get the current downloading torrents."""
await self.plex.downloading(ctx, parameters)
@cog_ext.cog_slash(**params["wolf"])
async def wolf(self, ctx: SlashContext, query):
"""Perform a search on Wolfram Alpha."""
await self.nerd_shit.wolf_search(ctx, query)
def setup(bot):
"""Add the cog to the bot."""
bot.add_cog(MiscCog(bot))

View File

@@ -0,0 +1,40 @@
"""Contains the StarWarsCog, which deals with Star Wars commands."""
from discord.ext import commands
from interactions import cog_ext
from gwendolyn_old.utils import get_params # pylint: disable=import-error
params = get_params()
class StarWarsCog(commands.Cog):
"""Contains the Star Wars commands."""
def __init__(self, bot):
"""Initialize the cog."""
self.bot = bot
@cog_ext.cog_slash(**params["star_wars_roll"])
async def star_wars_roll(self, ctx, dice=""):
"""Roll Star Wars dice."""
await self.bot.star_wars.roll.parseRoll(ctx, dice)
@cog_ext.cog_slash(**params["star_wars_destiny"])
async def star_wars_destiny(self, ctx, parameters=""):
"""Control Star Wars destiny points."""
await self.bot.star_wars.destiny.parseDestiny(ctx, parameters)
@cog_ext.cog_slash(**params["star_wars_crit"])
async def star_wars_crit(self, ctx, severity: int = 0):
"""Roll for critical injuries."""
await self.bot.star_wars.roll.critRoll(ctx, severity)
@cog_ext.cog_slash(**params["star_wars_character"])
async def star_wars_character(self, ctx, parameters=""):
"""Access and change Star Wars character sheet data."""
await self.bot.star_wars.character.parseChar(ctx, parameters)
def setup(bot):
"""Add the cog to the bot."""
bot.add_cog(StarWarsCog(bot))

View File

@@ -0,0 +1,11 @@
"""Exceptions for Gwendolyn"""
class GameNotInDatabase(Exception):
def __init__(self, game: str, channel: str):
self.message = f"There is no {game} game in channel {channel}"
super().__init__(self.message)
class InvalidInteraction(Exception):
def __init__(self, custom_id: str, decoded: str):
self.message = f"{custom_id = }, {decoded = }"
super().__init__(self.message)

View File

@@ -0,0 +1,11 @@
"""A collection of all Gwendolyn functions."""
__all__ = ["Games", "Money", "LookupFuncs", "StarWars"]
from .games import Money, Games
from .lookup import LookupFuncs
from .other import Other
from .star_wars_funcs import StarWars

View File

@@ -0,0 +1,6 @@
"""Functions for games Gwendolyn can play."""
__all__ = ["Money", "Games"]
from .money import Money
from .games_container import Games

View File

@@ -0,0 +1,966 @@
"""
Runs commands, game logic and imaging for blackjack games.
*Classes*
---------
Blackjack
Contains the blackjack game logic.
DrawBlackjack
Draws images of the blackjack table.
"""
import math # Used for flooring decimal numbers
import datetime # Used to generate the game id
import asyncio # Used for sleeping
from interactions import InteractionContext as IntCont # Used for
# typehints
from interactions import ComponentContext
from discord.abc import Messageable
from PIL import Image
from gwendolyn_old.utils import replace_multiple
from .game_base import CardGame, CardDrawer
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
RED = (255, 50, 50)
GOLD = (155, 123, 0)
def _is_round_done(game: dict):
"""
Find out if the round is done.
*Parameters*
------------
game: dict
The game to check.
*Returns*
---------
round_done: bool
Whether the round is done.
"""
return all(
hand["hit"] or hand["standing"]
for user_hands in game["user hands"].values() for hand in user_hands
)
class Blackjack(CardGame):
"""
Deals with blackjack commands and gameplay logic.
*Methods*
---------
hit(ctx: IntCont, hand_number: int = 0)
double(ctx: IntCont, hand_number: int = 0)
stand(ctx: IntCont, hand_number: int = 0)
split(ctx: IntCont, hand_number: int = 0)
enter_game(ctx: IntCont, bet: int)
start(ctx: IntCont)
hilo(ctx: IntCont)
shuffle(ctx: IntCont)
cards(ctx: IntCont)
"""
def __init__(self, bot):
"""Initialize the class."""
super().__init__(bot, "blackjack", DrawBlackjack, 4)
default_buttons = ["Hit", "Stand", "Double", "Split"]
self.default_buttons = [(i, [i, "0"], 1) for i in default_buttons]
async def _test_command(self, game: dict, user: str, command: str,
hand_number: int, ctx: IntCont): #pylint:disable=too-many-arguments
valid_command = False
if user not in game["user hands"]:
self.bot.log(f"They tried to {command} without being in the game")
send_msg = f"You have to enter the game before you can {command}"
elif len(game["user hands"][user]) < hand_number:
self.bot.log(f"They tried to {command} with a hand they don't have")
send_msg = "You don't have that many hands"
elif game["round"] <= 0:
self.bot.log(f"They tried to {command} on the 0th round")
send_msg = "You haven't seen your cards yet!"
elif len(game["user hands"][user]) > 1 and hand_number == 0:
self._get_hand_number(ctx, command, game["user hands"][user])
return False
elif game["user hands"][user][max(hand_number-1,0)]["hit"]:
self.bot.log("They've already hit this round")
send_msg = "You've already hit this round"
elif game["user hands"][user][max(hand_number-1,0)]["standing"]:
self.bot.log("They're already standing")
send_msg = "You're already standing"
else:
valid_command = True
if not valid_command:
await ctx.send(send_msg, hidden=True)
return valid_command
def _shuffle_cards(self, channel: str):
"""
Shuffle an amount of decks equal to self.decks_used.
The shuffled cards are placed in the database as the cards that
are used by other blackjack functions.
*Parameters*
------------
channel: str
The id of the channel where the cards will be used.
"""
super()._shuffle_cards(channel)
# Creates hilo file
self.bot.log(f"creating hilo doc for {channel}")
hilo_updater = {"$set": {"_id": channel, "hilo": 0}}
self._update_document(channel, hilo_updater, "hilo")
def _calc_hand_value(self, hand: list):
"""
Calculate the value of a blackjack hand.
*Parameters*
------------
hand: list
The hand to calculate the value of. Each element in the
list is a card represented as a string in the format of
"xy" where x is the number of the card (0, k, q, and j
for 10s, kings, queens and jacks) and y is the suit (d,
c, h or s).
*Returns*
---------
hand_value: int
The blackjack value of the hand.
"""
values = [0]
for card in hand:
card_value = replace_multiple(card[0], ["0", "k", "q", "j"], "10")
if card_value == "a":
values = [i + 1 for i in values] + [i + 11 for i in values]
else:
values = [i+int(card_value) for i in values]
valid_values = [i for i in values if i <= 21]
hand_value = max(valid_values) if valid_values else min(values)
self.bot.log(f"Calculated the value of {hand} to be {hand_value}")
return hand_value
def _draw_card(self, channel: str):
drawn_card = super()._draw_card(channel)
hilo_value = min(1,-1-((self._calc_hand_value([drawn_card])-10)//3))
self._update_document(channel, {"$inc": {"hilo": hilo_value}}, "hilo")
return drawn_card
def _dealer_draw(self, channel: str):
"""
Draw a card for the dealer.
*Parameters*
------------
channel: str
The id of the channel to draw a card for the dealer in.
*Returns*
---------
done: bool
Whether the dealer is done drawing cards.
"""
game = self.access_document(channel)
dealer_hand = game["dealer hand"]
if self._calc_hand_value(dealer_hand) < 17:
dealer_hand.append(self._draw_card(channel))
dealer_updater = {"$set": {"dealer hand": dealer_hand}}
self._update_document(channel, dealer_updater)
done = False
else:
done = True
if self._calc_hand_value(dealer_hand) > 21:
self._update_document(channel, {"$set": {"dealer busted": True}})
return done
def _blackjack_continue(self, channel: str):
"""
Continues the blackjack game to the next round.
*Parameters*
------------
channel: str
The id of the channel the blackjack game is in
*Returns*
---------
send_message: str
The message to send to the channel.
all_standing: bool
If all players are standing.
gameDone: bool
If the game has finished.
"""
self.bot.log("Continuing blackjack game")
game = self.access_document(channel)
self._update_document(channel, {"$inc": {"round": 1}})
message = self.long_strings["Blackjack all players standing"]
all_standing = True
pre_all_standing = True
done = False
if game["all standing"]:
self.bot.log("All are standing")
done = self._dealer_draw(channel)
message = "The dealer draws a card."
self.bot.log("Testing if all are standing")
for user, user_hands in game["user hands"].items():
standing_test = (self._test_if_standing(user_hands))
hand_updater = {"$set": {"user hands."+user: standing_test[0]}}
self._update_document(channel, hand_updater)
if not standing_test[1]:
all_standing = False
if not standing_test[2]:
pre_all_standing = False
if all_standing:
self._update_document(channel, {"$set": {"all standing": True}})
if done:
message = "The dealer is done drawing cards"
return message, True, done
if pre_all_standing:
return "", True, done
send_message = self.long_strings["Blackjack commands"]
if game["round"] == 0:
send_message = send_message.format(
self.long_strings["Blackjack first round"])
else:
send_message = send_message.format("")
return send_message, False, done
def _test_if_standing(self, user_hands: list):
"""
Test if a player is standing on all their hands.
Also resets the hand if it's not standing
*Parameters*
------------
hand: dict
The hands to test and reset.
*Returns*
---------
hand: dict
The reset hand.
all_standing: bool
If the player is standing on all their hands.
pre_all_standing: bool
Is true if all_standing is True, or if a player has done
something equivalent to standing but still needs to see
the newly drawn card.
"""
# If the user has not hit, they are by definition standing.
all_standing = True
pre_all_standing = True
for hand in user_hands:
if not hand["hit"]:
hand["standing"] = True
if not hand["standing"]:
all_standing = False
if self._calc_hand_value(hand["hand"]) >= 21 or hand["doubled"]:
hand["standing"] = True
else:
pre_all_standing = False
hand["hit"] = False
return user_hands, all_standing, pre_all_standing
async def _blackjack_finish(self, channel: Messageable):
"""
Generate the winnings message after the blackjack game ends.
*Parameters*
------------
channel: str
The id of the channel the game is in.
*Returns*
---------
final_winnings: str
The winnings message.
"""
final_winnings = "*Final Winnings:*\n"
game = self.access_document(str(channel.id))
for user in game["user hands"]:
winnings, net_winnings, reason = self._calc_winning(
game["user hands"][user],
self._calc_hand_value(game["dealer hand"]),
game["dealer blackjack"],
game["dealer busted"]
)
user_name = self.bot.database_funcs.get_name(user)
if winnings < 0:
final_winnings += f"{user_name} lost"
else:
final_winnings += f"{user_name} won"
if abs(winnings) == 1:
final_winnings += " 1 GwendoBuck"
else:
final_winnings += f" {abs(winnings)} GwendoBucks"
final_winnings += f" {reason}\n"
self.bot.money.addMoney(user, net_winnings)
await self._end_game(channel)
return final_winnings
def _calc_winning(self, user_hands: dict, dealer_value: int,
dealer_blackjack: bool, dealer_busted: bool):
"""
Calculate how much a user has won/lost in the blackjack game.
*Parameters*
------------
hand: dict
The hand to calculate the winnings of.
dealer_value: int
The dealer's hand value.
top_level: bool
If the input hand is _all_ if the player's hands. If
False, it's one of the hands resulting from a split.
dealer_blackjack: bool
If the dealer has a blackjack.
dealer_busted: bool
If the dealer busted.
*Returns*
---------
winnings: int
How much the player has won/lost.
net_winnings: int
winnings minus the original bet. This is added to the
user's account, since the bet was removed from their
account when they placed the bet.
reason: str
The reason for why they won/lost.
"""
self.bot.log("Calculating winnings")
reason = ""
winnings = 0
net_winnings = 0
for hand in user_hands:
winnings += -1 * hand["bet"]
hand_value = self._calc_hand_value(hand["hand"])
states = [
(dealer_blackjack, "dealer blackjack", 0),
(hand["blackjack"], "blackjack", math.floor(2.5 * hand["bet"])),
(hand["busted"], "busted", 0),
(dealer_busted, dealer_busted, 2*hand["bet"]),
(hand_value == dealer_value, "pushed", hand["bet"]),
(hand_value > dealer_value, "highest value", 2 * hand["bet"]),
(True, "highest value", 0)
]
for state in states:
if state[0]:
reason += f"({state[1]})"
net_winnings += state[2]
break
winnings += net_winnings
return winnings, net_winnings, reason
async def _blackjack_loop(self, channel, game_round: int, game_id: str):
"""
Run blackjack logic and continue if enough time passes.
*Parameters*
------------
channel: guildChannel or DMChannel
The channel the game is happening in.
game_round: int
The round to start.
game_id: str
The ID of the game.
"""
self.bot.log(f"Loop {game_round}", str(channel.id))
new_message, all_standing, game_done = self._blackjack_continue(
str(channel.id)
)
if new_message != "":
self.bot.log(new_message, str(channel.id))
await channel.send(new_message)
if not game_done:
await self._delete_old_image(channel)
buttons = None if all_standing else self.default_buttons
await self._send_image(channel, buttons)
await asyncio.sleep([120,5][all_standing])
if not self._test_document(str(channel.id)):
self.bot.log(f"Ending loop on round {game_round}", str(channel.id))
return
game = self.access_document(str(channel.id))
if game_round != game["round"] or game_id != game["game_id"]:
self.bot.log(f"Ending loop on round {game_round}", str(channel.id))
return
if not game_done:
log_message = f"Loop {game_round} starting a new blackjack loop"
self.bot.log(log_message, str(channel.id))
await self._blackjack_loop(channel, game_round+1, game_id)
else:
await channel.send(await self._blackjack_finish(channel))
async def _get_hand_number(self, ctx: IntCont, command: str,
hands_amount: int):
buttons = [
(str(i+1), [command, "0", str(i+1)], 1) for i in range(hands_amount)
]
await ctx.send(
f"Which hand do you want to {command}?",
hidden=True,
components=self._get_action_rows(buttons)
)
async def _hit(self, ctx: IntCont, hand_number: int = 0):
"""
Hit on a hand.
*Parameters*
------------
ctx: IntCont
The context of the command.
hand_number: int = 1
The number of the hand to hit.
"""
await self.bot.defer(ctx)
channel = str(ctx.channel_id)
user = f"#{ctx.author.id}"
game = self.access_document(channel)
if not await self._test_command(game, user, "hit", hand_number, ctx):
return
hand = game["user hands"][user][max(hand_number-1,0)]
hand["hand"].append(self._draw_card(channel))
hand["hit"] = True
hand_value = self._calc_hand_value(hand["hand"])
if hand_value > 21:
hand["busted"] = True
hand_path = f"user hands.{user}.{max(hand_number-1,0)}"
self._update_document(channel, {"$set": {hand_path: hand}})
game = self.access_document(channel)
await ctx.send(f"{ctx.author.display_name} hit")
self.bot.log("They succeeded")
if _is_round_done(game):
game_id = game["game_id"]
self.bot.log("Hit calling self._blackjack_loop()", channel)
await self._blackjack_loop(
ctx.channel, game["round"]+1, game_id
)
async def _double(self, ctx: IntCont, hand_number: int = 0):
"""
Double a hand.
*Parameters*
------------
ctx: IntCont
The context of the command.
hand_number: int = 0
The number of the hand to double.
"""
await self.bot.defer(ctx)
channel = str(ctx.channel_id)
user = f"#{ctx.author.id}"
game = self.access_document(channel)
balance = self.bot.money.checkBalance(user)
if not await self._test_command(game, user, "double", hand_number, ctx):
return
if len(game["user hands"][user][max(hand_number-1,0)]["hand"]) != 2:
await ctx.send("You can only double on the first round")
self.bot.log("They tried to double after round 1")
elif balance < game["user hands"][user][max(hand_number-1,0)]["bet"]:
await ctx.send("You can't double when you don't have enough money")
self.bot.log("They tried to double without having enough money")
else:
hand = game["user hands"][user][max(hand_number-1,0)]
self.bot.money.addMoney(user, -1 * hand["bet"])
hand["hand"].append(self._draw_card(channel))
hand["hit"] = True
hand["doubled"] = True
hand["bet"] += hand["bet"]
if self._calc_hand_value(hand["hand"]) > 21:
hand["busted"] = True
hand_path = f"user hands.{user}.{max(hand_number-1,0)}"
self._update_document(channel, {"$set": {hand_path: hand}})
game = self.access_document(channel)
user_name = self.bot.database_funcs.get_name(user)
send_message = self.long_strings["Blackjack double"]
await ctx.send(send_message.format(hand["bet"], user_name))
self.bot.log("They succeeded")
if _is_round_done(game):
game_id = game["game_id"]
self.bot.log("Double calling self._blackjack_loop()", channel)
await self._blackjack_loop(
ctx.channel, game["round"]+1, game_id
)
async def _stand(self, ctx: IntCont, hand_number: int = 0):
"""
Stand on a hand.
*Parameters*
------------
ctx: IntCont
The context of the command.
hand_number: int = 0
The number of the hand to stand on.
"""
await self.bot.defer(ctx)
channel = str(ctx.channel_id)
user = f"#{ctx.author.id}"
game = self.access_document(channel)
if not await self._test_command(game, user, "stand", hand_number, ctx):
return
hand = game["user hands"][user][max(hand_number-1,0)]
hand["standing"] = True
hand_path = f"user hands.{user}.{max(hand_number-1,0)}"
self._update_document(channel, {"$set": {hand_path: hand}})
game = self.access_document(channel)
send_message = f"{ctx.author.display_name} is standing"
log_message = "They succeeded"
await ctx.send(send_message)
self.bot.log(log_message)
if _is_round_done(game):
game_id = game["game_id"]
self.bot.log("Stand calling self._blackjack_loop()", channel)
await self._blackjack_loop(ctx.channel, game["round"]+1, game_id)
async def _split(self, ctx: IntCont, hand_number: int = 0):
"""
Split a hand.
*Parameters*
------------
ctx: IntCont
The context of the command.
hand_number: int = 0
The number of the hand to split.
"""
await self.bot.defer(ctx)
channel = str(ctx.channel_id)
user = f"#{ctx.author.id}"
game = self.access_document(channel)
if not await self._test_command(game, user, "split", hand_number, ctx):
return
old_hand = game["user hands"][user][max(hand_number-1,0)]
if len(game["user hands"][user]) > 3:
await ctx.send("You can only split 3 times")
self.bot.log("They tried to split more than three times")
elif len(old_hand["hand"]) != 2:
await ctx.send("You can only split on the first round")
self.bot.log("They tried to split after the first round")
elif len({self._calc_hand_value([i]) for i in old_hand["hand"]}) != 1:
await ctx.send(self.long_strings["Blackjack different cards"])
self.bot.log("They tried to split two different cards")
elif self.bot.money.checkBalance(user) < old_hand["bet"]:
await ctx.send("You don't have enough GwendoBucks")
self.bot.log("They didn't have enough GwendoBucks")
else:
self.bot.money.addMoney(user, -1 * old_hand["bet"])
old_hand["hit"] = True
hands = [old_hand, {
"hand": [old_hand["hand"].pop(1), self._draw_card(channel)],
"bet": old_hand["bet"], "standing": False,
"busted": False, "blackjack": False, "hit": True,
"doubled": False
}]
old_hand["hand"].append(self._draw_card(channel))
for i, hand in enumerate(hands):
if self._calc_hand_value(hand["hand"]) > 21:
hands[i]["busted"] = True
elif self._calc_hand_value(hand["hand"]) == 21:
hands[i]["blackjack"] = True
game["user hands"][user][max(hand_number-1,0)] = hands[0]
game["user hands"][user].append(hands[1])
updater = {"$set": {f"user hands.{user}": game["user hands"][user]}}
self._update_document(channel, updater)
send_message = self.long_strings["Blackjack split"]
user_name = self.bot.database_funcs.get_name(user)
await ctx.send(send_message.format(user_name))
self.bot.log("They succeeded")
game = self.access_document(channel)
if _is_round_done(game):
game_id = game["game_id"]
self.bot.log("Split calling self._blackjack_loop()", channel)
await self._blackjack_loop(
ctx.channel, game["round"]+1, game_id
)
async def enter_game(self, ctx: IntCont, bet: int):
"""
Enter the blackjack game.
*Parameters*
------------
ctx: IntCont
The context of the command.
bet: int
The bet to enter with.
"""
await self.bot.defer(ctx)
channel = str(ctx.channel_id)
user = f"#{ctx.author.id}"
game = self.access_document(channel)
user_name = self.bot.database_funcs.get_name(user)
self.bot.log(f"{user_name} is trying to join the Blackjack game")
if user in game["user hands"]:
await ctx.send("You're already in the game!")
self.bot.log("They're already in the game")
elif len(game["user hands"]) >= 5:
await ctx.send("There can't be more than 5 players in a game")
self.bot.log("There were already 5 players in the game")
elif game["round"] != 0:
await ctx.send("The table is no longer taking bets")
self.bot.log("They tried to join after the game begun")
elif bet < 0:
await ctx.send("You can't bet a negative amount")
self.bot.log("They tried to bet a negative amount")
elif self.bot.money.checkBalance(user) < bet:
await ctx.send("You don't have enough GwendoBucks")
self.bot.log("They didn't have enough GwendoBucks")
else:
self.bot.money.addMoney(user, -1 * bet)
player_hand = [self._draw_card(channel) for _ in range(2)]
new_hand = [{
"hand": player_hand, "bet": bet, "standing": False,
"busted": False,
"blackjack": self._calc_hand_value(player_hand) == 21,
"hit": True, "doubled": False
}]
updater = {"$set": {f"user hands.{user}": new_hand}}
self._update_document(channel, updater)
send_message = "{} entered the game with a bet of {} GwendoBucks"
await ctx.send(send_message.format(user_name, bet))
self.bot.log(send_message.format(user_name, bet))
async def start(self, ctx: IntCont):
"""
Start a blackjack game.
*Parameters*
------------
ctx: IntCont
The context of the command.
"""
await self.bot.defer(ctx)
channel = str(ctx.channel_id)
blackjack_min_cards = 50
self.bot.log("Starting blackjack game")
await ctx.send("Starting a new game of blackjack")
cards_left = 0
if self._test_document(channel, "cards"):
cards = self.access_document(channel, "cards")
cards_left = len(cards["cards"])
# Shuffles if not enough cards
if cards_left < blackjack_min_cards:
self._shuffle_cards(channel)
await ctx.channel.send("Shuffling the deck...")
self.bot.log(f"Trying to start a blackjack game in {channel}")
if self._test_document(channel):
await ctx.channel.send(self.long_strings["Blackjack going on"])
self.bot.log("There was already a game going on")
return
dealer_hand = [self._draw_card(channel), self._draw_card(channel)]
game_id = datetime.datetime.now().strftime('%Y%m%d%H%M%S')
new_game = {
"dealer hand": dealer_hand,
"dealer busted": False, "game_id": game_id,
"dealer blackjack": (self._calc_hand_value(dealer_hand) == 21),
"user hands": {}, "all standing": False, "round": 0
}
await ctx.channel.send(self.long_strings["Blackjack started"])
labels = [0] + [10**i for i in range(4)]
betting_buttons = [
(f"Bet {i}", ["bet", "0", str(i)], 1) for i in labels
]
await self._start_new(ctx.channel, new_game, betting_buttons)
await asyncio.sleep(30)
game = self.access_document(channel)
if len(game["user hands"]) == 0:
await ctx.channel.send("No one entered the game. Ending the game.")
await ctx.channel.send(await self._blackjack_finish(ctx.channel))
return
game_id = game["game_id"]
# Loop of game rounds
self.bot.log("start() calling _blackjack_loop()", channel)
await self._blackjack_loop(ctx.channel, 1, game_id)
async def hilo(self, ctx: IntCont):
"""
Get the hilo of the blackjack game.
*Parameters*
------------
ctx: IntCont
The context of the command.
"""
data = self.access_document(str(ctx.channel_id), "hilo", False)
hilo = data["hilo"] if data else 0
await ctx.send(f"Hi-lo value: {hilo}", hidden=True)
async def shuffle(self, ctx: IntCont):
"""
Shuffle the cards used for blackjack.
*Parameters*
------------
ctx: IntCont
The context of the command.
"""
self._shuffle_cards(str(ctx.channel_id))
await ctx.send("Shuffling the deck...")
async def cards(self, ctx: IntCont):
"""
Get how many cards are left for blackjack.
*Parameters*
------------
ctx: IntCont
The context of the command.
"""
cards = self.access_document(str(ctx.channel_id), "cards", False)
cards_left = len(cards["cards"]) if cards else 0
decks_left = round(cards_left/52, 1) if cards else 0
send_message = f"Cards left:\n{cards_left} cards, {decks_left} decks"
await ctx.send(send_message, hidden=True)
async def decode_interaction(self, ctx: ComponentContext, info: list[str]):
if info[0].lower() == "bet":
await self.enter_game(ctx, int(info[2]))
elif info[0].lower() == "hit":
await self._hit(ctx, int(info[2]) if len(info) > 2 else 0)
elif info[0].lower() == "stand":
await self._stand(ctx, int(info[2]) if len(info) > 2 else 0)
elif info[0].lower() == "double":
await self._double(ctx, int(info[2]) if len(info) > 2 else 0)
elif info[0].lower() == "split":
await self._split(ctx, int(info[2]) if len(info) > 2 else 0)
class DrawBlackjack(CardDrawer):
"""
Draws the blackjack image.
*Methods*
---------
*Attributes*
------------
bot: Gwendolyn
The instance of the bot.
PLACEMENT: list
The order to place the user hands in.
"""
# pylint: disable=too-few-public-methods
def __init__(self, bot, game: Blackjack):
"""Initialize the class."""
super().__init__(bot, game)
# pylint: disable=invalid-name
self.PLACEMENT = [2, 1, 3, 0, 4]
# pylint: enable=invalid-name
self._set_card_size(197)
def _draw_dealer_hand(self, game: dict, table: Image.Image):
dealer_hand = self._draw_hand(
game["dealer hand"],
not game["all standing"],
game["dealer busted"] if game["all standing"] else False,
game["dealer blackjack"] if game["all standing"] else False
)
paste_position = (800-self.CARD_BORDER, 20-self.CARD_BORDER)
table.paste(dealer_hand, paste_position, dealer_hand)
def _draw_player_name(self, user: str, placement: int, table: Image.Image):
user_name = self.bot.database_funcs.get_name(user)
font, font_size = self._adjust_font(50, user_name, 360)
text_width, text_height = font.getsize(user_name)
text_x = 32+(384*placement)+117-(text_width//2)
text_y = 960+text_height
colors = [BLACK, WHITE]
text_image = self._draw_shadow_text(user_name, colors, font_size)
table.paste(text_image, (text_x, text_y), text_image)
def _draw_player_hands(self, all_hands: dict, table: Image.Image):
for i, (user, user_hands) in enumerate(all_hands.items()):
hand_images = []
position_x = 32-self.CARD_BORDER+(384*self.PLACEMENT[i])
for hand in user_hands:
hand_images.append(self._draw_hand(
hand["hand"],
False,
hand["busted"],
hand["blackjack"]
))
for index, image in enumerate(hand_images):
position_y = 840
position_y -= self.CARD_BORDER + (140 * (len(user_hands)-index))
position = (position_x, position_y)
table.paste(image, position, image)
self._draw_player_name(user, self.PLACEMENT[i], table)
def _draw_image(self, game: dict, table: Image.Image):
"""
Draw the table image.
*Parameters*
------------
channel: str
The id of the channel the game is in.
"""
self._draw_dealer_hand(game, table)
self._draw_player_hands(game["user hands"], table)
def _draw_hand(self, hand: dict, dealer: bool, busted: bool,
blackjack: bool):
"""
Draw a hand.
*Parameters*
------------
hand: dict
The hand to draw.
dealer: bool
If the hand belongs to the dealer who hasn't shown
their second card.
busted: bool
If the hand is busted.
blackjack: bool
If the hand has a blackjack.
*Returns*
---------
background: Image
The image of the hand.
"""
self.bot.log("Drawing hand {hand}, {busted}, {blackjack}")
background = self._draw_cards(hand, int(dealer))
if busted or blackjack:
if busted:
text = "BUSTED"
last_color = RED
font_size = 60
elif blackjack:
text = "BLACKJACK"
last_color = GOLD
font_size = 35
colors = [BLACK, WHITE, last_color]
text_image = self._draw_shadow_text(text, colors, font_size)
width = background.size[0]
text_width = self._get_font(font_size).getsize(text)[0]
text_position = (int(width/2)-int(text_width/2), 85)
background.paste(text_image, text_position, text_image)
return background

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,318 @@
"""Base class for the games."""
import random
from typing import Union
from discord import File, User
from discord.abc import Messageable
from interactions import Button, ActionRow
from interactions import InteractionContext as IntCont
from PIL import ImageFont, Image, ImageDraw
from gwendolyn_old.exceptions import GameNotInDatabase
from gwendolyn_old.utils import encode_id
class GameBase():
"""The base class for the games."""
def __init__(self, bot, game_name: int, drawer):
"""Initialize the class."""
self.bot = bot
self.long_strings = self.bot.long_strings
self.resources = "gwendolyn/resources/games/"
self.game_name = game_name
self.draw = drawer(bot, self)
def _get_action_rows(self, buttons: list[tuple[str, list]]):
self.bot.log("Generation action rows")
button_objects = []
for label, data, style in buttons:
custom_id = encode_id([self.game_name] + data)
button_objects.append(Button(
style=style,
label=label,
custom_id=custom_id
))
action_rows = []
for i in range(((len(button_objects)-1)//5)+1):
action_rows.append(ActionRow(*button_objects[i*5:i*5+5]))
return action_rows
async def _send_image(self, channel: Messageable,
buttons: list[tuple[str, list]] = None, delete=True):
self.draw.draw(str(channel.id))
file_path = f"{self.resources}images/{self.game_name}{channel.id}.png"
old_image = await channel.send(
file=File(file_path), delete_after=120 if delete else None)
if buttons is not None and len(buttons) < 25:
await old_image.edit(components = self._get_action_rows(buttons))
return old_image
class DatabaseGame(GameBase):
"""The base class for the games."""
def __init__(self, bot, game_name, drawer):
"""Initialize the class."""
super().__init__(bot, game_name, drawer)
self.database = self.bot.database
self.old_images_path = f"{self.resources}old_images/{self.game_name}"
def access_document(self, channel: str, collection_name: str="games",
raise_missing_error: bool=True):
collection = self.bot.database[f"{self.game_name} {collection_name}"]
game = collection.find_one({"_id": channel})
if game is None and raise_missing_error:
raise GameNotInDatabase(self.game_name, channel)
return game
def _test_document(self, channel: str, collection_name: str="games"):
collection = self.bot.database[f"{self.game_name} {collection_name}"]
game = collection.find_one({"_id": channel})
return game is not None
def _update_document(self, channel: str, updater: dict,
collection_name: str="games"):
self.database[f"{self.game_name} {collection_name}"].update_one(
{"_id": channel},
updater,
upsert=True
)
def _delete_document(self, channel: str, collection_name: str="games"):
self.database[f"{self.game_name} {collection_name}"].delete_one(
{"_id": channel}
)
def _insert_document(self, data: dict, collection_name: str="games"):
self.database[f"{self.game_name} {collection_name}"].insert_one(data)
async def _delete_old_image(self, channel: Messageable):
with open(self.old_images_path + str(channel.id), "r") as file_pointer:
old_image = await channel.fetch_message(int(file_pointer.read()))
await old_image.delete()
async def _send_image(self, channel: Messageable,
buttons: list[tuple[str, list]] = None, delete=True):
old_image = await super()._send_image(channel, buttons, delete)
with open(self.old_images_path + str(channel.id), "w") as file_pointer:
file_pointer.write(str(old_image.id))
async def _start_new(self, channel: Messageable, new_game: dict,
buttons: list[tuple[str, list]] = None,
delete = True):
new_game['_id'] = str(channel.id)
self._insert_document(new_game)
await self._send_image(channel, buttons, delete)
async def _end_game(self, channel: Messageable):
await self._delete_old_image(channel)
await self._send_image(channel, delete=False)
self._delete_document(str(channel.id))
class CardGame(DatabaseGame):
"""The class for card games."""
def __init__(self, bot, game_name, drawer, deck_used):
"""Initialize the class."""
super().__init__(bot, game_name, drawer)
self.decks_used = deck_used
def _shuffle_cards(self, channel: str):
self.bot.log(f"Shuffling cards for {self.game_name}")
with open(f"{self.resources}deck_of_cards.txt", "r") as file_pointer:
deck = file_pointer.read()
all_decks = deck.split("\n") * self.decks_used
random.shuffle(all_decks)
self._update_document(
channel,
{"$set": {"_id": channel, "cards": all_decks}},
"cards"
)
def _draw_card(self, channel: str):
"""
Draw a card from the stack.
*Parameters*
------------
channel: str
The id of the channel the card is drawn in.
"""
self.bot.log("drawing a card")
drawn_card = self.access_document(channel, "cards")["cards"][0]
self._update_document(channel, {"$pop": {"cards": -1}}, "cards")
return drawn_card
class BoardGame(GameBase):
async def _test_opponent(self, ctx: IntCont, opponent: Union[int, User]):
if isinstance(opponent, int):
# Opponent is Gwendolyn
if opponent in range(1, 6):
difficulty = int(opponent)
difficulty_text = f" with difficulty {difficulty}"
opponent = self.bot.user.id
else:
await ctx.send("Difficulty doesn't exist")
self.bot.log("They challenged a difficulty that doesn't exist")
return False
elif isinstance(opponent, User):
if opponent.bot:
# User has challenged a bot
if opponent == self.bot.user:
# It was Gwendolyn
difficulty = 3
difficulty_text = f" with difficulty {difficulty}"
opponent = self.bot.user.id
else:
await ctx.send("You can't challenge a bot!")
self.bot.log("They tried to challenge a bot")
return False
else:
# Opponent is another player
if ctx.author != opponent:
opponent = opponent.id
difficulty = 5
difficulty_text = ""
else:
await ctx.send("You can't play against yourself")
self.bot.log("They tried to play against themself")
return False
return opponent, (difficulty, difficulty_text)
class BaseDrawer():
"""Class for drawing games."""
def __init__(self, bot, game: GameBase):
self.bot = bot
self.game = game
self.fonts_path = "gwendolyn/resources/fonts/"
self.font_name = "futura-bold"
self.resources = game.resources
game_name = game.game_name
self.default_image = f"{self.resources}default_images/{game_name}.png"
self.default_size = None
self.default_color = None
self.images_path = f"{self.resources}images/{game_name}"
def _get_size(self, game: dict):
return self.default_size
def _draw_image(self, game: dict, image: Image.Image):
pass
def draw(self, channel: str):
game = self.game.access_document(channel)
if self.default_image is not None:
image = Image.open(self.default_image)
else:
image = Image.new("RGB", self._get_size(game), self.default_color)
self._draw_image(game, image)
self._save_image(image, channel)
def _get_font(self, size: int, font_name: str = None):
if font_name is None:
font_name = self.font_name
return ImageFont.truetype(f"{self.fonts_path}{font_name}.ttf", size)
def _adjust_font(self, max_font_size: int, text: str, max_text_size: int,
font_name: str = None):
font_size = max_font_size
font = self._get_font(font_size, font_name)
text_width = font.getsize(text)[0]
while text_width > max_text_size:
font_size -= 1
font = self._get_font(font_size, font_name)
text_width = font.getsize(text)[0]
return font, font_size
def _save_image(self, image: Image.Image, channel: str):
self.bot.log("Saving image")
image.save(f"{self.images_path}{channel}.png")
def _draw_shadow_text(self, text: str, colors: list[tuple[int, int, int]],
font_size: int):
font = self._get_font(font_size)
offset = font_size//20
shadow_offset = font_size//10
text_size = list(font.getsize(text))
text_size[0] += 1 + (offset * 2)
text_size[1] += 1 + (offset * 2) + shadow_offset
image = Image.new("RGBA", tuple(text_size), (0, 0, 0, 0))
text_image = ImageDraw.Draw(image)
for color_index, color in enumerate(colors[:-1]):
color_offset = offset//(color_index+1)
if color_index == 0:
color_shadow_offset = shadow_offset
else:
color_shadow_offset = 0
for i in range(4):
x_pos = [-1,1][i % 2] * color_offset
y_pos = [-1,1][(i//2) % 2] * color_offset + color_shadow_offset
position = (offset + x_pos, offset + y_pos)
text_image.text(position, text, fill=color, font=font)
text_image.text((offset, offset), text, fill=colors[-1], font=font)
return image
class CardDrawer(BaseDrawer):
def __init__(self, bot, game: GameBase):
super().__init__(bot, game)
self.cards_path = f"{self.resources}cards/"
# pylint: disable=invalid-name
self.CARD_WIDTH = 691
self.CARD_HEIGHT = 1065
self.CARD_BORDER = 100
self.CARD_OFFSET = 125
# pylint: enable=invalid-name
def _set_card_size(self, wanted_card_width: int):
ratio = wanted_card_width / self.CARD_WIDTH
self.CARD_WIDTH = wanted_card_width
self.CARD_HEIGHT = int(self.CARD_HEIGHT * ratio)
self.CARD_BORDER = int(self.CARD_BORDER * ratio)
self.CARD_OFFSET = int(self.CARD_OFFSET * ratio)
def _draw_cards(self, hand: list[str], hidden_cards: int = 0):
image_width = (self.CARD_BORDER * 2) + self.CARD_WIDTH
image_width += (self.CARD_OFFSET * (len(hand)-1))
image_size = (image_width, (self.CARD_BORDER * 2) + self.CARD_HEIGHT)
background = Image.new("RGBA", image_size, (0, 0, 0, 0))
for i, card in enumerate(hand):
position = (
self.CARD_BORDER + (self.CARD_OFFSET*i),
self.CARD_BORDER
)
if i + hidden_cards < len(hand):
card_image = Image.open(f"{self.cards_path}{card.upper()}.png")
else:
card_image = Image.open(f"{self.cards_path}red_back.png")
card_image = card_image.resize(
(self.CARD_WIDTH, self.CARD_HEIGHT),
resample=Image.BILINEAR
)
background.paste(card_image, position, card_image)
return background

View File

@@ -0,0 +1,46 @@
"""
Has a container for game functions.
*Classes*
---------
Games
Container for game functions.
"""
from .trivia import Trivia
from .blackjack import Blackjack
from .connect_four import ConnectFour
from .hangman import Hangman
from .hex import HexGame
from .wordle import WordleGame
class Games():
"""
Contains game classes.
*Attributes*
------------
bot: Gwendolyn
The instance of Gwendolyn.
blackjack
Contains blackjack functions.
connect_four
Contains connect four functions.
hangman
Contains hangman functions.
hex
Contains hex functions
"""
def __init__(self, bot):
"""Initialize the container."""
self.bot = bot
self.trivia = Trivia(bot)
self.blackjack = Blackjack(bot)
self.connect_four = ConnectFour(bot)
self.hangman = Hangman(bot)
self.hex = HexGame(bot)
self.wordle = WordleGame(bot)

View File

@@ -0,0 +1,657 @@
"""
Deals with commands and logic for hangman games.
*Classes*
---------
Hangman()
Deals with the game logic of hangman.
DrawHangman()
Draws the image shown to the player.
"""
import os
import datetime # Used for generating the game id
import string # string.ascii_uppercase used
import math # Used by DrawHangman(), mainly for drawing circles
import random # Used to draw poorly
import requests # Used for getting the word in Hangman.start()
import discord # Used for discord.file and type hints
from interactions.utils.manage_components import (create_button,
create_actionrow)
from interactions.model import ButtonStyle
from interactions import SlashContext, ComponentContext
# Used for typehints
from PIL import ImageDraw, Image, ImageFont # Used to draw the image
from gwendolyn_old.utils import encode_id
class Hangman():
"""
Controls hangman commands and game logic.
*Methods*
---------
start(ctx: SlashContext)
stop(ctx: SlashContext)
guess(message: discord.message, user: str, guess: str)
"""
def __init__(self, bot):
"""
Initialize the class.
*Attributes*
------------
draw: DrawHangman
The DrawHangman used to draw the hangman image.
API_url: str
The url to get the words from.
APIPARAMS: dict
The parameters to pass to every api call.
"""
self.bot = bot
self._draw = DrawHangman(bot)
self._API_url = "https://api.wordnik.com/v4/words.json/randomWords?" # pylint: disable=invalid-name
api_key = self.bot.credentials["wordnik_key"]
self._APIPARAMS = { # pylint: disable=invalid-name
"hasDictionaryDef": True,
"minCorpusCount": 5000,
"maxCorpusCount": -1,
"minDictionaryCount": 1,
"maxDictionaryCount": -1,
"minLength": 3,
"maxLength": 11,
"limit": 1,
"api_key": api_key
}
async def start(self, ctx: SlashContext):
"""
Start a game of hangman.
*Parameters*
------------
ctx: SlashContext
The context of the command.
"""
await self.bot.defer(ctx)
word = "-"
while "-" in word or "." in word:
response = requests.get(self._API_url, params=self._APIPARAMS)
word = list(response.json()[0]["word"].upper())
self.bot.log("Found the word \""+"".join(word)+"\"")
guessed = [False] * len(word)
game_id = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
remaining_letters = list(string.ascii_uppercase)
self._draw.draw_image(game_id, 0, word, guessed, [])
send_message = f"{ctx.author.display_name} started a game of hangman."
self.bot.log("Game started")
await ctx.send(send_message)
boards_path = "gwendolyn/resources/games/hangman_boards/"
file_path = f"{boards_path}hangman_board{game_id}.png"
image_message = await ctx.channel.send(
file=discord.File(file_path)
)
blank_message_one = await ctx.channel.send("_ _")
blank_message_two = await ctx.channel.send("_ _")
buttons = []
for letter in remaining_letters:
custom_id = encode_id([
"hangman",
"guess",
str(ctx.author.id),
letter,
"".join(word),
"",
game_id,
str(image_message.id),
str(blank_message_one.id),
str(blank_message_two.id)
])
buttons.append(create_button(
style=ButtonStyle.blue,
label=letter,
custom_id=custom_id
))
custom_id = encode_id([
"hangman",
"end",
str(ctx.author.id),
game_id,
str(image_message.id),
str(blank_message_one.id),
str(blank_message_two.id)
])
buttons.append(create_button(
style=ButtonStyle.red,
label="End game",
custom_id=custom_id
))
action_row_lists = []
for i in range(((len(buttons)-1)//20)+1):
action_rows = []
available_buttons = buttons[(i*20):(min(len(buttons),i*20+20))]
for x in range(((len(available_buttons)-1)//4)+1):
row_buttons = available_buttons[
(x*4):(min(len(available_buttons),x*4+4))
]
action_rows.append(create_actionrow(*row_buttons))
action_row_lists.append(action_rows)
await blank_message_one.edit(components=action_row_lists[0])
await blank_message_two.edit(components=action_row_lists[1])
async def stop(self, ctx: ComponentContext, game_id: str,
*messages: list[str]):
"""
Stop the game of hangman.
*Parameters*
------------
ctx: SlashContext
The context of the command.
"""
for msg_id in messages:
msg = await ctx.channel.fetch_message(msg_id)
await msg.delete()
self.bot.log("Deleting old messages")
await ctx.channel.send("Hangman game stopped")
boards_path = "gwendolyn/resources/games/hangman_boards/"
file_path = f"{boards_path}hangman_board{game_id}.png"
os.remove(file_path)
async def guess(self, ctx: ComponentContext, guess: str, word: str,
guessed_letters: str, game_id: str, *messages: list[str]):
"""
Guess a letter.
*Parameters*
------------
message: discord.Message
The message that the reaction was placed on.
user: str
The id of the user.
guess: str
The guess.
"""
for msg_id in messages:
msg = await ctx.channel.fetch_message(msg_id)
await msg.delete()
misses = 0
guessed_letters += guess
guessed = [False for i in range(len(word))]
for guessed_letter in guessed_letters:
correct_guess = 0
for i, letter in enumerate(word):
if guessed_letter == letter:
correct_guess += 1
guessed[i] = True
if correct_guess == 0:
misses += 1
remaining_letters = list(string.ascii_uppercase)
for letter in guessed_letters:
remaining_letters.remove(letter)
if correct_guess == 1:
send_message = "Guessed {}. There was 1 {} in the word."
send_message = send_message.format(guess, guess)
else:
send_message = "Guessed {}. There were {} {}s in the word."
send_message = send_message.format(guess, correct_guess, guess)
self._draw.draw_image(game_id, misses, word, guessed, guessed_letters)
if misses == 6:
send_message += self.bot.long_strings["Hangman lost game"]
remaining_letters = []
elif all(guessed):
self.bot.money.addMoney(f"#{ctx.author_id}", 15)
send_message += self.bot.long_strings["Hangman guessed word"]
remaining_letters = []
if remaining_letters != []:
await ctx.channel.send(send_message)
boards_path = "gwendolyn/resources/games/hangman_boards/"
file_path = f"{boards_path}hangman_board{game_id}.png"
image_message = await ctx.channel.send(
file=discord.File(file_path)
)
blank_message_one = await ctx.channel.send("_ _")
blank_message_two = await ctx.channel.send("_ _")
buttons = []
for letter in list(string.ascii_uppercase):
custom_id = encode_id([
"hangman",
"guess",
str(ctx.author.id),
letter,
word,
guessed_letters,
game_id,
str(image_message.id),
str(blank_message_one.id),
str(blank_message_two.id)
])
buttons.append(create_button(
style=ButtonStyle.blue,
label=letter,
custom_id=custom_id,
disabled=(letter not in remaining_letters)
))
custom_id = encode_id([
"hangman",
"end",
str(ctx.author.id),
game_id,
str(image_message.id),
str(blank_message_one.id),
str(blank_message_two.id)
])
buttons.append(create_button(
style=ButtonStyle.red,
label="End game",
custom_id=custom_id
))
action_row_lists = []
for i in range(((len(buttons)-1)//20)+1):
action_rows = []
available_buttons = buttons[(i*20):(min(len(buttons),i*20+20))]
for x in range(((len(available_buttons)-1)//4)+1):
row_buttons = available_buttons[
(x*4):(min(len(available_buttons),x*4+4))
]
action_rows.append(create_actionrow(*row_buttons))
action_row_lists.append(action_rows)
await blank_message_one.edit(components=action_row_lists[0])
await blank_message_two.edit(components=action_row_lists[1])
else:
boards_path = "gwendolyn/resources/games/hangman_boards/"
file_path = f"{boards_path}hangman_board{game_id}.png"
await ctx.channel.send(send_message, file=discord.File(file_path))
os.remove(file_path)
class DrawHangman():
"""
Draws the image of the hangman game.
*Methods*
---------
draw_image(channel: str)
"""
def __init__(self, bot):
"""
Initialize the class.
*Attributes*
------------
CIRCLESIZE
LINEWIDTH
BODYSIZE
LIMBSIZE
ARMPOSITION
MANX, MANY
LETTERLINELENGTH
LETTERLINEDISTANCE
GALLOWX, GALLOWY
PHI
FONT
SMALLFONT
"""
self._bot = bot
# pylint: disable=invalid-name
self._CIRCLESIZE = 120
self._LINEWIDTH = 12
self._BODYSIZE = 210
self._LIMBSIZE = 60
self._ARMPOSITION = 60
self._MANX = (self._LIMBSIZE*2)
self._MANY = (self._CIRCLESIZE+self._BODYSIZE+self._LIMBSIZE)
MANPADDING = self._LINEWIDTH*4
self._MANX += MANPADDING
self._MANY += MANPADDING
self._LETTERLINELENGTH = 90
self._LETTERLINEDISTANCE = 30
self._GALLOWX, self._GALLOWY = 360, 600
self._PHI = 1-(1 / ((1 + 5 ** 0.5) / 2))
LETTERSIZE = 75 # Wrong guesses letter size
WORDSIZE = 70 # Correct guesses letter size
FONTPATH = "gwendolyn/resources/fonts/comic-sans-bold.ttf"
self._FONT = ImageFont.truetype(FONTPATH, LETTERSIZE)
self._SMALLFONT = ImageFont.truetype(FONTPATH, WORDSIZE)
# pylint: enable=invalid-name
def _deviate(self, pre_deviance: int, pre_deviance_accuracy: int,
position_change: float, maxmin: int,
max_acceleration: float):
random_deviance = random.uniform(-position_change, position_change)
deviance_accuracy = pre_deviance_accuracy + random_deviance
if deviance_accuracy > maxmin * max_acceleration:
deviance_accuracy = maxmin * max_acceleration
elif deviance_accuracy < -maxmin * max_acceleration:
deviance_accuracy = -maxmin * max_acceleration
deviance = pre_deviance + deviance_accuracy
if deviance > maxmin:
deviance = maxmin
elif deviance < -maxmin:
deviance = -maxmin
return deviance, deviance_accuracy
def _bad_circle(self):
circle_padding = (self._LINEWIDTH*3)
image_width = self._CIRCLESIZE+circle_padding
image_size = (image_width, image_width)
background = Image.new("RGBA", image_size, color=(0, 0, 0, 0))
drawer = ImageDraw.Draw(background, "RGBA")
middle = (self._CIRCLESIZE+(self._LINEWIDTH*3))/2
deviance_x = 0
deviance_y = 0
deviance_accuracy_x = 0
deviance_accuracy_y = 0
start = random.randint(-100, -80)
degrees_amount = 360 + random.randint(-10, 30)
for degree in range(degrees_amount):
deviance_x, deviance_accuracy_x = self._deviate(
deviance_x,
deviance_accuracy_x,
self._LINEWIDTH/100,
self._LINEWIDTH,
0.03
)
deviance_y, deviance_accuracy_y = self._deviate(
deviance_y,
deviance_accuracy_y,
self._LINEWIDTH/100,
self._LINEWIDTH,
0.03
)
radians = math.radians(degree+start)
circle_x = (math.cos(radians) * (self._CIRCLESIZE/2))
circle_y = (math.sin(radians) * (self._CIRCLESIZE/2))
position_x = middle + circle_x - (self._LINEWIDTH/2) + deviance_x
position_y = middle + circle_y - (self._LINEWIDTH/2) + deviance_y
circle_position = [
(position_x, position_y),
(position_x+self._LINEWIDTH, position_y+self._LINEWIDTH)
]
drawer.ellipse(circle_position, fill=(0, 0, 0, 255))
return background
def _bad_line(self, length: int, rotated: bool = False):
if rotated:
width, height = length+self._LINEWIDTH*3, self._LINEWIDTH*3
else:
width, height = self._LINEWIDTH*3, length+self._LINEWIDTH*3
background = Image.new("RGBA", (width, height), color=(0, 0, 0, 0))
drawer = ImageDraw.Draw(background, "RGBA")
possible_deviance = int(self._LINEWIDTH/3)
deviance_x = random.randint(-possible_deviance, possible_deviance)
deviance_y = 0
deviance_accuracy_x = 0
deviance_accuracy_y = 0
for pixel in range(length):
deviance_x, deviance_accuracy_x = self._deviate(
deviance_x,
deviance_accuracy_x,
self._LINEWIDTH/1000,
self._LINEWIDTH,
0.004
)
deviance_y, deviance_accuracy_y = self._deviate(
deviance_y,
deviance_accuracy_y,
self._LINEWIDTH/1000,
self._LINEWIDTH,
0.004
)
if rotated:
position_x = self._LINEWIDTH + pixel + deviance_x
position_y = self._LINEWIDTH + deviance_y
else:
position_x = self._LINEWIDTH + deviance_x
position_y = self._LINEWIDTH + pixel + deviance_y
circle_position = [
(position_x, position_y),
(position_x+self._LINEWIDTH, position_y+self._LINEWIDTH)
]
drawer.ellipse(circle_position, fill=(0, 0, 0, 255))
return background
def _draw_man(self, misses: int, seed: str):
random.seed(seed)
man_size = (self._MANX, self._MANY)
background = Image.new("RGBA", man_size, color=(0, 0, 0, 0))
if misses >= 1:
head = self._bad_circle()
paste_x = (self._MANX-(self._CIRCLESIZE+(self._LINEWIDTH*3)))//2
paste_position = (paste_x, 0)
background.paste(head, paste_position, head)
if misses >= 2:
body = self._bad_line(self._BODYSIZE)
paste_x = (self._MANX-(self._LINEWIDTH*3))//2
paste_position = (paste_x, self._CIRCLESIZE)
background.paste(body, paste_position, body)
if misses >= 3:
limbs = random.sample(["rl", "ll", "ra", "la"], min(misses-2, 4))
else:
limbs = []
random.seed(seed)
for limb in limbs:
limb_drawing = self._bad_line(self._LIMBSIZE, True)
x_position = (self._MANX-(self._LINEWIDTH*3))//2
if limb[1] == "a":
rotation = random.randint(-45, 45)
shift = math.sin(math.radians(rotation))
line_length = self._LIMBSIZE+(self._LINEWIDTH*3)
compensation = int(shift*line_length)
limb_drawing = limb_drawing.rotate(rotation, expand=1)
y_position = self._CIRCLESIZE + self._ARMPOSITION
if limb == "ra":
compensation = min(-compensation, 0)
else:
x_position -= self._LIMBSIZE
compensation = min(compensation, 0)
y_position += compensation
else:
rotation = random.randint(-15, 15)
y_position = self._CIRCLESIZE+self._BODYSIZE-self._LINEWIDTH
if limb == "rl":
limb_drawing = limb_drawing.rotate(rotation-45, expand=1)
else:
x_position += -limb_drawing.size[0]+self._LINEWIDTH*3
limb_drawing = limb_drawing.rotate(rotation+45, expand=1)
paste_position = (x_position, y_position)
background.paste(limb_drawing, paste_position, limb_drawing)
return background
def _bad_text(self, text: str, big: bool, color: tuple = (0, 0, 0, 255)):
if big:
font = self._FONT
else:
font = self._SMALLFONT
width, height = font.getsize(text)
img = Image.new("RGBA", (width, height), color=(0, 0, 0, 0))
drawer = ImageDraw.Draw(img, "RGBA")
drawer.text((0, 0), text, font=font, fill=color)
return img
def _draw_gallows(self):
gallow_size = (self._GALLOWX, self._GALLOWY)
background = Image.new("RGBA", gallow_size, color=(0, 0, 0, 0))
bottom_line = self._bad_line(int(self._GALLOWX * 0.75), True)
bottom_line_x = int(self._GALLOWX * 0.125)
bottom_line_y = self._GALLOWY-(self._LINEWIDTH*4)
paste_position = (bottom_line_x, bottom_line_y)
background.paste(bottom_line, paste_position, bottom_line)
line_two = self._bad_line(self._GALLOWY-self._LINEWIDTH*6)
line_two_x = int(self._GALLOWX*(0.75*self._PHI))
line_two_y = self._LINEWIDTH*2
paste_position = (line_two_x, line_two_y)
background.paste(line_two, paste_position, line_two)
top_line = self._bad_line(int(self._GALLOWY*0.30), True)
paste_x = int(self._GALLOWX*(0.75*self._PHI))-self._LINEWIDTH
paste_position = (paste_x, self._LINEWIDTH*3)
background.paste(top_line, paste_position, top_line)
last_line = self._bad_line(int(self._GALLOWY*0.125))
paste_x += int(self._GALLOWY*0.30)
background.paste(last_line, (paste_x, self._LINEWIDTH*3), last_line)
return background
def _draw_letter_lines(self, word: str, guessed: list, misses: int):
letter_width = self._LETTERLINELENGTH+self._LETTERLINEDISTANCE
image_width = letter_width*len(word)
image_size = (image_width, self._LETTERLINELENGTH+self._LINEWIDTH*3)
letter_lines = Image.new("RGBA", image_size, color=(0, 0, 0, 0))
for i, letter in enumerate(word):
line = self._bad_line(self._LETTERLINELENGTH, True)
paste_x = i*(self._LETTERLINELENGTH+self._LETTERLINEDISTANCE)
paste_position = (paste_x, self._LETTERLINELENGTH)
letter_lines.paste(line, paste_position, line)
if guessed[i]:
letter_drawing = self._bad_text(letter, True)
letter_width = self._FONT.getsize(letter)[0]
letter_x = i*(self._LETTERLINELENGTH+self._LETTERLINEDISTANCE)
letter_x -= (letter_width//2)
letter_x += (self._LETTERLINELENGTH//2)+(self._LINEWIDTH*2)
letter_lines.paste(
letter_drawing,
(letter_x, 0),
letter_drawing
)
elif misses == 6:
letter_drawing = self._bad_text(letter, True, (242, 66, 54))
letter_width = self._FONT.getsize(letter)[0]
letter_x = i*(self._LETTERLINELENGTH+self._LETTERLINEDISTANCE)
letter_x -= (letter_width//2)
letter_x += (self._LETTERLINELENGTH//2)+(self._LINEWIDTH*2)
letter_lines.paste(
letter_drawing,
(letter_x, 0),
letter_drawing
)
return letter_lines
def _shortest_dist(self, positions: list, new_position: tuple):
shortest_dist = math.inf
for i, j in positions:
x_distance = abs(i-new_position[0])
y_distance = abs(j-new_position[1])
dist = math.sqrt(x_distance**2+y_distance**2)
if shortest_dist > dist:
shortest_dist = dist
return shortest_dist
def _draw_misses(self, guesses: list, word: str):
background = Image.new("RGBA", (600, 400), color=(0, 0, 0, 0))
pos = []
for guess in guesses:
if guess not in word:
placed = False
while not placed:
letter = self._bad_text(guess, True)
w, h = self._FONT.getsize(guess)
x = random.randint(0, 600-w)
y = random.randint(0, 400-h)
if self._shortest_dist(pos, (x, y)) > 70:
pos.append((x, y))
background.paste(letter, (x, y), letter)
placed = True
return background
def draw_image(self, game_id: str, misses: int, word: str,
guessed: list[bool], guessed_letters: str):
"""
Draw a hangman Image.
*Parameters*
------------
channel: str
The id of the channel the game is in.
"""
self._bot.log("Drawing hangman image")
random.seed(game_id)
background = Image.open("gwendolyn/resources/games/default_images/hangman.png")
gallow = self._draw_gallows()
man = self._draw_man(misses, game_id)
random.seed(game_id)
letter_line_parameters = [word, guessed, misses]
letter_lines = self._draw_letter_lines(*letter_line_parameters)
random.seed(game_id)
misses = self._draw_misses(list(guessed_letters), word)
background.paste(gallow, (100, 100), gallow)
background.paste(man, (300, 210), man)
background.paste(letter_lines, (120, 840), letter_lines)
background.paste(misses, (600, 150), misses)
misses_text = self._bad_text("MISSES", False)
misses_text_width = misses_text.size[0]
background.paste(misses_text, (850-misses_text_width//2, 50), misses_text)
boards_path = "gwendolyn/resources/games/hangman_boards/"
image_path = f"{boards_path}hangman_board{game_id}.png"
background.save(image_path)

View File

@@ -0,0 +1,642 @@
import random
import copy
import math
import discord
from PIL import Image, ImageDraw, ImageFont
class HexGame():
def __init__(self, bot):
self.bot = bot
self.draw = DrawHex(bot)
self.BOARDWIDTH = 11
self.ALLPOSITIONS = [(i,j) for i in range(11) for j in range(11)]
self.ALLSET = set(self.ALLPOSITIONS)
self.EMPTYDIJKSTRA = {}
for position in self.ALLPOSITIONS:
self.EMPTYDIJKSTRA[position] = math.inf # an impossibly high number
self.HEXDIRECTIONS = [(0,1),(-1,1),(-1,0),(0,-1),(1,-1),(1,0)]
async def surrender(self, ctx):
channel = str(ctx.channel_id)
game = self.bot.database["hex games"].find_one({"_id":channel})
user = f"#{ctx.author.id}"
players = game["players"]
if user not in players:
await ctx.send("You can't surrender when you're not a player.")
else:
opponent = (players.index(user) + 1) % 2
opponent_name = self.bot.database_funcs.get_name(players[opponent])
self.bot.database["hex games"].update_one({"_id":channel},{"$set":{"winner":opponent + 1}})
await ctx.send(f"{ctx.author.display_name} surrendered")
with open(f"gwendolyn/resources/games/old_images/hex{channel}", "r") as f:
old_image = await ctx.channel.fetch_message(int(f.read()))
if old_image is not None:
await old_image.delete()
else:
self.bot.log("The old image was already deleted")
self.bot.log("Sending the image")
file_path = f"gwendolyn/resources/games/hex_boards/board{channel}.png"
old_image = await ctx.channel.send(file = discord.File(file_path))
with open(f"gwendolyn/resources/games/old_images/hex{channel}", "w") as f:
f.write(str(old_image.id))
self.bot.database["hex games"].delete_one({"_id":channel})
# Swap
async def swap(self, ctx):
channel = str(ctx.channel_id)
game = self.bot.database["hex games"].find_one({"_id":channel})
user = f"#{ctx.author.id}"
if game is None:
await ctx.send("You can't swap nothing")
elif user not in game["players"]:
await ctx.send("You're not in the game")
elif len(game["gameHistory"]) != 1: # Only after the first move
await ctx.send("You can only swap as the second player after the very first move.")
elif user != game["players"][game["turn"]-1]:
await ctx.send("You can only swap after your opponent has placed their piece")
else:
self.bot.database["hex games"].update_one({"_id":channel},
{"$set":{"players":game["players"][::-1]}}) # Swaps their player-number
# Swaps the color of the hexes on the board drawing:
self.draw.drawSwap(channel)
opponent = game["players"][::-1][game["turn"]-1]
gwendolyn_turn = (opponent == f"#{self.bot.user.id}")
opponent_name = self.bot.database_funcs.get_name(opponent)
await ctx.send(f"The color of the players were swapped. It is now {opponent_name}'s turn")
with open(f"gwendolyn/resources/games/old_images/hex{channel}", "r") as f:
old_image = await ctx.channel.fetch_message(int(f.read()))
if old_image is not None:
await old_image.delete()
else:
self.bot.log("The old image was already deleted")
self.bot.log("Sending the image")
file_path = f"gwendolyn/resources/games/hex_boards/board{channel}.png"
old_image = await ctx.channel.send(file = discord.File(file_path))
with open(f"gwendolyn/resources/games/old_images/hex{channel}", "w") as f:
f.write(str(old_image.id))
if gwendolyn_turn:
await self.hexAI(ctx)
# Starts the game
async def start(self, ctx, opponent):
await self.bot.defer(ctx)
user = f"#{ctx.author.id}"
channel = str(ctx.channel_id)
game = self.bot.database["hex games"].find_one({"_id":channel})
started_game = False
can_start = True
if game != None:
send_message = "There's already a hex game going on in this channel"
log_message = "There was already a game going on"
can_start = False
else:
if type(opponent) == int:
# Opponent is Gwendolyn
if opponent in range(1, 6):
opponent_name = "Gwendolyn"
difficulty = int(opponent)
difficulty_text = f" with difficulty {difficulty}"
opponent = f"#{self.bot.user.id}"
else:
send_message = "Difficulty doesn't exist"
log_message = "They tried to play against a difficulty that doesn't exist"
can_start = False
elif type(opponent) == discord.member.Member:
if opponent.bot:
# User has challenged a bot
if opponent == self.bot.user:
# It was Gwendolyn
opponent_name = "Gwendolyn"
difficulty = 2
difficulty_text = f" with difficulty {difficulty}"
opponent = f"#{self.bot.user.id}"
else:
send_message = "You can't challenge a bot!"
log_message = "They tried to challenge a bot"
can_start = False
else:
# Opponent is another player
if ctx.author != opponent:
opponent_name = opponent.display_name
opponent = f"#{opponent.id}"
difficulty = 5
difficulty_text = ""
else:
send_message = "You can't play against yourself"
log_message = "They tried to play against themself"
can_start = False
else:
can_start = False
log_message = f"Opponent was neither int or member. It was {type(opponent)}"
send_message = "Something went wrong"
if can_start:
# board is 11x11
board = [[0 for i in range(self.BOARDWIDTH)] for j in range(self.BOARDWIDTH)]
players = [user, opponent]
random.shuffle(players) # random starting player
gameHistory = []
new_game = {"_id":channel,"board":board, "winner":0,
"players":players, "turn":1, "difficulty":difficulty, "gameHistory":gameHistory}
self.bot.database["hex games"].insert_one(new_game)
# draw the board
self.draw.drawBoard(channel)
gwendolyn_turn = (players[0] == f"#{self.bot.user.id}")
started_game = True
turn_name = self.bot.database_funcs.get_name(players[0])
send_message = f"Started Hex game against {opponent_name}{difficulty_text}. It's {turn_name}'s turn"
log_message = "Game started"
await ctx.send(send_message)
self.bot.log(log_message)
if started_game:
file_path = f"gwendolyn/resources/games/hex_boards/board{ctx.channel_id}.png"
new_image = await ctx.channel.send(file = discord.File(file_path))
with open(f"gwendolyn/resources/games/old_images/hex{ctx.channel_id}", "w") as f:
f.write(str(new_image.id))
if gwendolyn_turn:
await self.hexAI(ctx)
# Places a piece at the given location and checks things afterwards
async def placeHex(self, ctx, position : str, user):
channel = str(ctx.channel_id)
game = self.bot.database["hex games"].find_one({"_id":channel})
placed_piece = False
if game == None:
send_message = "There's no game in this channel"
self.bot.log("There was no game going on")
elif not (position[0].isalpha() and position[1:].isnumeric() and len(position) in [2, 3]):
send_message = "The position must be a letter followed by a number."
self.bot.log(f"The position was not valid, {position}")
else:
players = game["players"]
if user not in players:
send_message = f"You can't place when you're not in the game. The game's players are: {self.bot.database_funcs.get_name(game['players'][0])} and {self.bot.database_funcs.get_name(game['players'][1])}."
self.bot.log("They aren't in the game")
elif players[game["turn"]-1] != user:
send_message = "It's not your turn"
self.bot.log("It wasn't their turn")
else:
player = game["turn"]
turn = game["turn"]
board = game["board"]
self.bot.log("Placing a piece on the board with placeHex()")
# Places on board
board = self.placeOnHexBoard(board,player,position)
if board is None:
self.bot.log("It was an invalid position")
send_message = ("That's an invalid position. You must place your piece on an empty field.")
else:
# If the move is valid:
self.bot.database["hex games"].update_one({"_id":channel},{"$set":{"board":board}})
turn = (turn % 2) + 1
self.bot.database["hex games"].update_one({"_id":channel},{"$set":{"turn":turn}})
# Checking for a win
self.bot.log("Checking for win")
winner = self.evaluateBoard(game["board"])[1]
if winner == 0: # Continue with the game.
game_won = False
send_message = self.bot.database_funcs.get_name(game["players"][player-1])+" placed at "+position.upper()+". It's now "+self.bot.database_funcs.get_name(game["players"][turn-1])+"'s turn."# The score is "+str(score)
else: # Congratulations!
game_won = True
self.bot.database["hex games"].update_one({"_id":channel},{"$set":{"winner":winner}})
send_message = self.bot.database_funcs.get_name(game["players"][player-1])+" placed at "+position.upper()+" and won!"
if game["players"][winner-1] != f"#{self.bot.user.id}":
win_amount = game["difficulty"]*10
send_message += " Adding "+str(win_amount)+" GwendoBucks to their account."
self.bot.database["hex games"].update_one({"_id":channel},
{"$push":{"gameHistory":(int(position[1])-1, ord(position[0])-97)}})
# Is it now Gwendolyn's turn?
gwendolyn_turn = False
if game["players"][turn-1] == f"#{self.bot.user.id}":
self.bot.log("It's Gwendolyn's turn")
gwendolyn_turn = True
placed_piece = True
if user == f"#{self.bot.user.id}":
await ctx.channel.send(send_message)
else:
await ctx.send(send_message)
if placed_piece:
# Update the board
self.draw.drawHexPlacement(channel,player, position)
with open(f"gwendolyn/resources/games/old_images/hex{channel}", "r") as f:
old_image = await ctx.channel.fetch_message(int(f.read()))
if old_image is not None:
await old_image.delete()
else:
self.bot.log("The old image was already deleted")
self.bot.log("Sending the image")
file_path = f"gwendolyn/resources/games/hex_boards/board{channel}.png"
old_image = await ctx.channel.send(file = discord.File(file_path))
if game_won:
self.bot.log("Dealing with the winning player")
game = self.bot.database["hex games"].find_one({"_id":channel})
winner = game["winner"]
if game["players"][winner-1] != f"#{self.bot.user.id}":
winnings = game["difficulty"]*10
self.bot.money.addMoney(game["players"][winner-1].lower(),winnings)
self.bot.database["hex games"].delete_one({"_id":channel})
else:
with open(f"gwendolyn/resources/games/old_images/hex{channel}", "w") as f:
f.write(str(old_image.id))
if gwendolyn_turn:
await self.hexAI(ctx)
# Returns a board where the placement has ocurred
def placeOnHexBoard(self, board,player,position):
# Translates the position
position = position.lower()
# Error handling
column = ord(position[0]) - 97 # ord() translates from letter to number
row = int(position[1:]) - 1
if column not in range(self.BOARDWIDTH) or row not in range(self.BOARDWIDTH):
self.bot.log("Position out of bounds")
return None
# Place at the position
if board[row][column] == 0:
board[row][column] = player
return board
else:
self.bot.log("Cannot place on existing piece")
return None
# After your move, you have the option to undo get your turn back #TimeTravel
async def undo(self, ctx):
channel = str(ctx.channel_id)
user = f"#{ctx.author.id}"
undid = False
game = self.bot.database["hex games"].find_one({"_id":channel})
if user not in game["players"]:
send_message = "You're not a player in the game"
elif len(game["gameHistory"]) == 0:
send_message = "You can't undo nothing"
elif user != game["players"][(game["turn"] % 2)]: # If it's not your turn
send_message = "It's not your turn"
else:
turn = game["turn"]
self.bot.log("Undoing {}'s last move".format(self.bot.database_funcs.get_name(user)))
lastMove = game["gameHistory"].pop()
game["board"][lastMove[0]][lastMove[1]] = 0
self.bot.database["hex games"].update_one({"_id":channel},
{"$set":{"board":game["board"]}})
self.bot.database["hex games"].update_one({"_id":channel},
{"$set":{"turn":turn%2 + 1}})
# Update the board
self.draw.drawHexPlacement(channel,0,"abcdefghijk"[lastMove[1]]+str(lastMove[0]+1)) # The zero makes the hex disappear
send_message = f"You undid your last move at {lastMove}"
undid = True
await ctx.send(send_message)
if undid:
with open(f"gwendolyn/resources/games/old_images/hex{channel}", "r") as f:
old_image = await ctx.channel.fetch_message(int(f.read()))
if old_image is not None:
await old_image.delete()
else:
self.bot.log("The old image was already deleted")
self.bot.log("Sending the image")
file_path = f"gwendolyn/resources/games/hex_boards/board{channel}.png"
old_image = await ctx.channel.send(file = discord.File(file_path))
with open(f"gwendolyn/resources/games/old_images/hex{channel}", "w") as f:
f.write(str(old_image.id))
# Plays as the AI
async def hexAI(self, ctx):
channel = str(ctx.channel_id)
self.bot.log("Figuring out best move")
game = self.bot.database["hex games"].find_one({"_id":channel})
board = game["board"]
if len(game["gameHistory"]):
lastMove = game["gameHistory"][-1]
else:
lastMove = (5,5)
# These moves are the last move +- 2.
moves = [[(lastMove[0]+j-2,lastMove[1]+i-2) for i in range(5) if lastMove[1]+i-2 in range(11)] for j in range(5) if lastMove[0]+j-2 in range(11)]
moves = sum(moves,[])
movesCopy = moves.copy()
for move in movesCopy:
if board[move[0]][move[1]] != 0:
moves.remove(move)
chosenMove = random.choice(moves)
placement = "abcdefghijk"[chosenMove[1]]+str(chosenMove[0]+1)
self.bot.log(f"ChosenMove is {chosenMove} at {placement}")
await self.placeHex(ctx, placement, f"#{self.bot.user.id}")
def evaluateBoard(self, board):
scores = {1:0, 2:0}
winner = 0
# Here, I use Dijkstra's algorithm to evaluate the board, as proposed by this article: https://towardsdatascience.com/hex-creating-intelligent-adversaries-part-2-heuristics-dijkstras-algorithm-597e4dcacf93
for player in [1,2]:
Distance = copy.deepcopy(self.EMPTYDIJKSTRA)
# Initialize the starting hexes. For the blue player, this is the leftmost column. For the red player, this is the tom row.
for start in (self.ALLPOSITIONS[::11] if player == 2 else self.ALLPOSITIONS[:11]):
# An empty hex adds a of distance of 1. A hex of own color add distance 0. Opposite color adds infinite distance.
Distance[start] = 1 if (board[start[0]][start[1]] == 0) else 0 if (board[start[0]][start[1]] == player) else math.inf
visited = set() # Also called sptSet, short for "shortest path tree Set"
for _ in range(self.BOARDWIDTH**2): # We can at most check every 121 hexes
# Find the next un-visited hex, that has the lowest distance
remainingHexes = self.ALLSET.difference(visited)
A = [Distance[k] for k in remainingHexes] # Find the distance to each un-visited hex
u = list(remainingHexes)[A.index(min(A))] # Chooses the one with the lowest distance
# Find neighbors of the hex u
for di in self.HEXDIRECTIONS:
v = (u[0] + di[0] , u[1] + di[1]) # v is a neighbor of u
if v[0] in range(11) and v[1] in range(11) and v not in visited:
new_dist = Distance[u] + (1 if (board[v[0]][v[1]] == 0) else 0 if (board[v[0]][v[1]] == player) else math.inf)
Distance[v] = min(Distance[v], new_dist)
# After a hex has been visited, this is noted
visited.add(u)
#self.bot.log("Distance from player {}'s start to {} is {}".format(player,u,Distance[u]))
if u[player-1] == 10: # if the right coordinate of v is 10, it means we're at the goal
scores[player] = Distance[u] # A player's score is the shortest distance to goal. Which equals the number of remaining moves they need to win if unblocked by the opponent.
break
else:
self.bot.log("For some reason, no path to the goal was found. ")
if scores[player] == 0:
winner = player
break # We don't need to check the other player's score, if player1 won.
return scores[2]-scores[1], winner
def minimaxHex(self, board, depth, alpha, beta, maximizing_player):
# The depth is how many moves ahead the computer checks. This value is the difficulty.
if depth == 0 or 0 not in sum(board,[]):
score = self.evaluateBoard(board)[0]
return score
# if final depth is not reached, look another move ahead:
if maximizing_player: # red player predicts next move
maxEval = -math.inf
possiblePlaces = [i for i,v in enumerate(sum(board,[])) if v == 0]
#self.bot.log("Judging a red move at depth {}".format(depth))
for i in possiblePlaces:
test_board = copy.deepcopy(board)
test_board[i // self.BOARDWIDTH][i % self.BOARDWIDTH] = 1 # because maximizing_player is Red which is number 1
evaluation = self.minimaxHex(test_board,depth-1,alpha,beta,False)
maxEval = max(maxEval, evaluation)
alpha = max(alpha, evaluation)
if beta <= alpha:
#self.bot.log("Just pruned something!")
break
return maxEval
else: # blue player predicts next move
minEval = math.inf
possiblePlaces = [i for i,v in enumerate(sum(board,[])) if v == 0]
#self.bot.log("Judging a blue move at depth {}".format(depth))
for i in possiblePlaces:
test_board = copy.deepcopy(board)
test_board[i // self.BOARDWIDTH][i % self.BOARDWIDTH] = 2 # because minimizingPlayer is Blue which is number 2
evaluation = self.minimaxHex(test_board,depth-1,alpha,beta,True)
minEval = min(minEval, evaluation)
beta = min(beta, evaluation)
if beta <= alpha:
#self.bot.log("Just pruned something!")
break
return minEval
class DrawHex():
def __init__(self,bot):
self.bot = bot
# Defining all the variables
self.CANVASWIDTH = 2400
self.CANVASHEIGHT = 1800
self.SIDELENGTH = 75
# The offsets centers the board in the picture
self.XOFFSET = self.CANVASWIDTH/2 - 8*math.sqrt(3)*self.SIDELENGTH
# The offsets are the coordinates of the upperleft point in the
# upperleftmost hexagon
self.YOFFSET = self.CANVASHEIGHT/2 - 8*self.SIDELENGTH
# The whole width of one hexagon
self.HEXAGONWIDTH = math.sqrt(3) * self.SIDELENGTH
# The height difference between two layers
self.HEXAGONHEIGHT = 1.5 * self.SIDELENGTH
self.FONTSIZE = 45
self.TEXTCOLOR = (0,0,0)
self.FONT = ImageFont.truetype('gwendolyn/resources/fonts/futura-bold.ttf', self.FONTSIZE)
self.LINETHICKNESS = 15
self.HEXTHICKNESS = 6 # This is half the width of the background lining between every hex
self.XTHICKNESS = self.HEXTHICKNESS * math.cos(math.pi/6)
self.YTHICKNESS = self.HEXTHICKNESS * math.sin(math.pi/6)
self.BACKGROUNDCOLOR = (230,230,230)
self.BETWEENCOLOR = (231,231,231)
self.BLANKCOLOR = "lightgrey"
self.PIECECOLOR = {1:(237,41,57),2:(0,165,255),0:self.BLANKCOLOR} # player1 is red, player2 is blue
self.BOARDCOORDINATES = [ [(self.XOFFSET + self.HEXAGONWIDTH*(column + row/2),self.YOFFSET + self.HEXAGONHEIGHT*row) for column in range(11)] for row in range(11)] # These are the coordinates for the upperleft corner of every hex
self.COLHEXTHICKNESS = 4 # When placing a hex, it is a little bigger than the underlying hex in the background (4 < 6)
self.COLXTHICKNESS = self.COLHEXTHICKNESS * math.cos(math.pi/6)
self.COLYTHICKNESS = self.COLHEXTHICKNESS * math.sin(math.pi/6)
# The Name display things:
self.NAMESIZE = 60
self.NAMEFONT = ImageFont.truetype('gwendolyn/resources/fonts/futura-bold.ttf', self.NAMESIZE)
self.XNAME = {1:175, 2:self.CANVASWIDTH-100}
self.YNAME = {1:self.CANVASHEIGHT-150, 2:150}
self.NAMEHEXPADDING = 90
self.SMALLWIDTH = self.HEXAGONWIDTH * 0.6
self.SMALLSIDELENGTH = self.SIDELENGTH * 0.6
def drawBoard(self, channel):
self.bot.log("Drawing empty Hex board")
# Creates the empty image
im = Image.new('RGB', size=(self.CANVASWIDTH, self.CANVASHEIGHT),color = self.BACKGROUNDCOLOR)
# 'd' is a shortcut to drawing on the image
d = ImageDraw.Draw(im,"RGBA")
# Drawing all the hexagons
for column in self.BOARDCOORDINATES:
for startingPoint in column:
x = startingPoint[0]
y = startingPoint[1]
d.polygon([
(x, y),
(x+self.HEXAGONWIDTH/2, y-0.5*self.SIDELENGTH),
(x+self.HEXAGONWIDTH, y),
(x+self.HEXAGONWIDTH, y+self.SIDELENGTH),
(x+self.HEXAGONWIDTH/2, y+1.5*self.SIDELENGTH),
(x, y+self.SIDELENGTH),
],fill = self.BETWEENCOLOR)
d.polygon([
(x+self.XTHICKNESS, y + self.YTHICKNESS),
(x+self.HEXAGONWIDTH/2, y-0.5*self.SIDELENGTH + self.HEXTHICKNESS),
(x+self.HEXAGONWIDTH-self.XTHICKNESS, y + self.YTHICKNESS),
(x+self.HEXAGONWIDTH-self.XTHICKNESS, y+self.SIDELENGTH - self.YTHICKNESS),
(x+self.HEXAGONWIDTH/2, y+1.5*self.SIDELENGTH - self.HEXTHICKNESS),
(x+self.XTHICKNESS, y+self.SIDELENGTH - self.YTHICKNESS),
],fill = self.BLANKCOLOR)
# Draw color on the outside of the board
# Top line, red
d.line(sum((sum([(point[0],point[1],point[0]+self.HEXAGONWIDTH/2,point[1]-self.HEXAGONHEIGHT+self.SIDELENGTH) for point in self.BOARDCOORDINATES[0]],()),(self.BOARDCOORDINATES[0][10][0]+self.HEXAGONWIDTH*3/4,self.BOARDCOORDINATES[0][10][1]-self.SIDELENGTH/4)),()),
fill = self.PIECECOLOR[1],width = self.LINETHICKNESS)
# Bottom line, red
d.line(sum(((self.BOARDCOORDINATES[10][0][0]+self.HEXAGONWIDTH/4,self.BOARDCOORDINATES[10][0][1]+self.SIDELENGTH*5/4),sum([(point[0]+self.HEXAGONWIDTH/2,point[1]+self.HEXAGONHEIGHT,point[0]+self.HEXAGONWIDTH,point[1]+self.SIDELENGTH) for point in self.BOARDCOORDINATES[10]],())),()),
fill = self.PIECECOLOR[1],width = self.LINETHICKNESS)
# Left line, blue
d.line(sum((sum([(row[0][0],row[0][1],row[0][0],row[0][1]+self.SIDELENGTH) for row in self.BOARDCOORDINATES],()),(self.BOARDCOORDINATES[10][0][0]+self.HEXAGONWIDTH/4,self.BOARDCOORDINATES[10][0][1]+self.SIDELENGTH*5/4)),()),
fill = self.PIECECOLOR[2],width = self.LINETHICKNESS)
# Right line, blue
d.line(sum(((self.BOARDCOORDINATES[0][10][0]+self.HEXAGONWIDTH*3/4,self.BOARDCOORDINATES[0][10][1]-self.SIDELENGTH/4),sum([(row[10][0]+self.HEXAGONWIDTH,row[10][1],row[10][0]+self.HEXAGONWIDTH,row[10][1]+self.SIDELENGTH) for row in self.BOARDCOORDINATES],())),()),
fill = self.PIECECOLOR[2],width = self.LINETHICKNESS)
# Writes "abc..", "123.." on the columns and rows
for i in range(11):
# Top letters
d.text( (self.XOFFSET + self.HEXAGONWIDTH*i, self.YOFFSET-66) , "ABCDEFGHIJK"[i], font=self.FONT, fill=self.TEXTCOLOR)
# Bottom letters
d.text( (self.XOFFSET + self.HEXAGONWIDTH*(i+11.5/2), self.YOFFSET - 15 + 11*self.HEXAGONHEIGHT) , "ABCDEFGHIJK"[i], font=self.FONT, fill=self.TEXTCOLOR)
# Left numbers
d.multiline_text( (self.XOFFSET + self.HEXAGONWIDTH*i/2 - 72 -4*(i>8), self.YOFFSET + 18 + i*self.HEXAGONHEIGHT) , str(i+1), font=self.FONT, fill=self.TEXTCOLOR, align="right")
# Right numbers
d.text( (self.XOFFSET + self.HEXAGONWIDTH*(i/2+11) + 30 , self.YOFFSET + 6 + i*self.HEXAGONHEIGHT) , str(i+1), font=self.FONT, fill=self.TEXTCOLOR)
# Write player names and color
game = self.bot.database["hex games"].find_one({"_id":channel})
for p in [1,2]:
playername = self.bot.database_funcs.get_name(game["players"][p-1])
# Draw name
x = self.XNAME[p]
x -= self.NAMEFONT.getsize(playername)[0] if p==2 else 0 # player2's name is right-aligned
y = self.YNAME[p]
d.text((x,y),playername, font=self.NAMEFONT, fill = self.TEXTCOLOR)
# Draw a half-size Hexagon to indicate the player's color
x -= self.NAMEHEXPADDING # To the left of both names
d.polygon([
(x, y),
(x+self.SMALLWIDTH/2, y-self.SMALLSIDELENGTH/2),
(x+self.SMALLWIDTH, y),
(x+self.SMALLWIDTH, y+self.SMALLSIDELENGTH),
(x+self.SMALLWIDTH/2, y+self.SMALLSIDELENGTH*3/2),
(x, y+self.SMALLSIDELENGTH),
],fill = self.PIECECOLOR[p])
im.save("gwendolyn/resources/games/hex_boards/board"+channel+".png")
def drawHexPlacement(self, channel,player,position):
FILEPATH = "gwendolyn/resources/games/hex_boards/board"+channel+".png"
self.bot.log(f"Drawing a newly placed hex. Filename: board{channel}.png")
# Translates position
# We don't need to error-check, because the position is already checked in placeOnHexBoard()
position = position.lower()
column = ord(position[0])-97 # ord() translates from letter to number
row = int(position[1:])-1
# Find the coordinates for the filled hex drawing
hex_coords = [
(self.BOARDCOORDINATES[row][column][0]+self.COLXTHICKNESS, self.BOARDCOORDINATES[row][column][1] + self.COLYTHICKNESS),
(self.BOARDCOORDINATES[row][column][0]+self.HEXAGONWIDTH/2, self.BOARDCOORDINATES[row][column][1]-0.5*self.SIDELENGTH + self.COLHEXTHICKNESS),
(self.BOARDCOORDINATES[row][column][0]+self.HEXAGONWIDTH-self.COLXTHICKNESS, self.BOARDCOORDINATES[row][column][1] + self.COLYTHICKNESS),
(self.BOARDCOORDINATES[row][column][0]+self.HEXAGONWIDTH-self.COLXTHICKNESS, self.BOARDCOORDINATES[row][column][1]+self.SIDELENGTH - self.COLYTHICKNESS),
(self.BOARDCOORDINATES[row][column][0]+self.HEXAGONWIDTH/2, self.BOARDCOORDINATES[row][column][1]+1.5*self.SIDELENGTH - self.COLHEXTHICKNESS),
(self.BOARDCOORDINATES[row][column][0]+self.COLXTHICKNESS, self.BOARDCOORDINATES[row][column][1]+self.SIDELENGTH - self.COLYTHICKNESS),
]
# Opens the image
try:
with Image.open(FILEPATH) as im:
d = ImageDraw.Draw(im,"RGBA")
# Draws the hex piece
d.polygon(hex_coords,fill = self.PIECECOLOR[player], outline = self.BETWEENCOLOR)
# Save
im.save(FILEPATH)
except:
self.bot.log("Error drawing new hex on board")
def drawSwap(self, channel):
FILEPATH = "gwendolyn/resources/games/hex_boards/board"+channel+".png"
game = self.bot.database["hex games"].find_one({"_id":channel})
# Opens the image
try:
with Image.open(FILEPATH) as im:
d = ImageDraw.Draw(im,"RGBA")
# Write player names and color
for p in [1,2]:
playername = self.bot.database_funcs.get_name(game["players"][p%2])
x = self.XNAME[p]
x -= self.NAMEFONT.getsize(playername)[0] if p==2 else 0 # player2's name is right-aligned
y = self.YNAME[p]
# Draw a half-size Hexagon to indicate the player's color
x -= self.NAMEHEXPADDING # To the left of both names
d.polygon([
(x, y),
(x+self.SMALLWIDTH/2, y-self.SMALLSIDELENGTH/2),
(x+self.SMALLWIDTH, y),
(x+self.SMALLWIDTH, y+self.SMALLSIDELENGTH),
(x+self.SMALLWIDTH/2, y+self.SMALLSIDELENGTH*3/2),
(x, y+self.SMALLSIDELENGTH),
],fill = self.PIECECOLOR[p % 2 + 1])
# Save
im.save(FILEPATH)
except:
self.bot.log("Error drawing swap")

View File

@@ -0,0 +1,151 @@
"""
Contains the code that deals with money.
*Classes*
---------
Money
Deals with money.
"""
import interactions # Used for typehints
import discord # Used for typehints
class Money():
"""
Deals with money.
*Methods*
---------
checkBalance(user: str)
sendBalance(ctx: interactions.SlashContext)
addMoney(user: str, amount: int)
giveMoney(ctx: interactions.SlashContext, user: discord.User,
amount: int)
*Attributes*
------------
bot: Gwendolyn
The instance of Gwendolyn
database: pymongo.Client
The mongo database
"""
def __init__(self, bot):
"""Initialize the class."""
self.bot = bot
self.database = bot.database
def checkBalance(self, user: str):
"""
Get the account balance of a user.
*Parameters*
------------
user: str
The user to get the balance of.
*Returns*
---------
balance: int
The balance of the user's account.
"""
self.bot.log("checking "+user+"'s account balance")
user_data = self.database["users"].find_one({"_id": user})
if user_data is not None:
return user_data["money"]
else:
return 0
async def sendBalance(self, ctx: interactions.SlashContext):
"""
Get your own account balance.
*Parameters*
------------
ctx: interactions.SlashContext
The context of the command.
"""
await self.bot.defer(ctx)
response = self.checkBalance("#"+str(ctx.author.id))
user_name = ctx.author.display_name
if response == 1:
new_message = f"{user_name} has {response} GwendoBuck"
else:
new_message = f"{user_name} has {response} GwendoBucks"
await ctx.send(new_message)
# Adds money to the account of a user
def addMoney(self, user: str, amount: int):
"""
Add money to a user account.
*Parameters*
------------
user: str
The id of the user to give money.
amount: int
The amount to add to the user's account.
"""
self.bot.log("adding "+str(amount)+" to "+user+"'s account")
user_data = self.database["users"].find_one({"_id": user})
if user_data is not None:
updater = {"$inc": {"money": amount}}
self.database["users"].update_one({"_id": user}, updater)
else:
new_user = {
"_id": user,
"user name": self.bot.database_funcs.get_name(user),
"money": amount
}
self.database["users"].insert_one(new_user)
# Transfers money from one user to another
async def giveMoney(self, ctx: interactions.SlashContext,
user: discord.User, amount: int):
"""
Give someone else money from your account.
*Parameters*
------------
ctx: interactions.SlashContext
The context of the command.
user: discord.User
The user to give money.
amount: int
The amount to transfer.
"""
await self.bot.defer(ctx)
username = user.display_name
if self.bot.database_funcs.get_id(username) is None:
async for member in ctx.guild.fetch_members(limit=None):
if member.display_name.lower() == username.lower():
username = member.display_name
user_id = f"#{member.id}"
new_user = {
"_id": user_id,
"user name": username,
"money": 0
}
self.bot.database["users"].insert_one(new_user)
userid = f"#{ctx.author.id}"
user_data = self.database["users"].find_one({"_id": userid})
targetUser = self.bot.database_funcs.get_id(username)
if amount <= 0:
self.bot.log("They tried to steal")
await ctx.send("Yeah, no. You can't do that")
elif targetUser is None:
self.bot.log("They weren't in the system")
await ctx.send("The target doesn't exist")
elif user_data is None or user_data["money"] < amount:
self.bot.log("They didn't have enough GwendoBucks")
await ctx.send("You don't have that many GwendoBuck")
else:
self.addMoney(f"#{ctx.author.id}", -1 * amount)
self.addMoney(targetUser, amount)
await ctx.send(f"Transferred {amount} GwendoBucks to {username}")

View File

@@ -0,0 +1,205 @@
"""
Contains code for trivia games.
*Classes*
---------
Trivia
Contains all the code for the trivia commands.
"""
import urllib # Used to get data from api
import json # Used to read data from api
import random # Used to shuffle answers
import asyncio # Used to sleep
from interactions import SlashContext # Used for type hints
class Trivia():
"""
Contains the code for trivia games.
*Methods*
---------
triviaStart(channel: str) -> str, str, str
triviaAnswer(user: str, channel: str, command: str) -> str
triviaCountPoints(channel: str)
triviaParse(ctx: SlashContext, answer: str)
"""
def __init__(self, bot):
"""Initialize the class."""
self.bot = bot
def triviaStart(self, channel: str):
"""
Start a game of trivia.
Downloads a question with answers, shuffles the wrong answers
with the correct answer and returns the questions and answers.
Also saves the question in the database.
*Parameters*
------------
channel: str
The id of the channel to start the game in
*Returns*
---------
send_message: str
The message to return to the user.
"""
triviaQuestions = self.bot.database["trivia questions"]
question = triviaQuestions.find_one({"_id": channel})
self.bot.log(f"Trying to find a trivia question for {channel}")
if question is None:
apiUrl = "https://opentdb.com/api.php?amount=10&type=multiple"
with urllib.request.urlopen(apiUrl) as response:
data = json.loads(response.read())
question = data["results"][0]["question"]
self.bot.log(f"Found the question \"{question}\"")
answers = data["results"][0]["incorrect_answers"]
answers.append(data["results"][0]["correct_answer"])
random.shuffle(answers)
correctAnswer = data["results"][0]["correct_answer"]
correctAnswer = answers.index(correctAnswer) + 97
newQuestion = {
"_id": channel,
"answer": str(chr(correctAnswer)),
"players": {}
}
triviaQuestions.insert_one(newQuestion)
replacements = {
"&#039;": "\'",
"&quot;": "\"",
"&ldquo;": "\"",
"&rdquo;": "\"",
"&eacute;": "é"
}
question = data["results"][0]["question"]
for key, value in replacements.items():
question = question.replace(key, value)
for answer in answers:
for key, value in replacements.items():
answer = answer.replace(key, value)
return question, answers, correctAnswer
else:
log_message = "There was already a trivia question for that channel"
self.bot.log(log_message)
return self.bot.long_strings["Trivia going on"], "", ""
def triviaAnswer(self, user: str, channel: str, command: str):
"""
Answer the current trivia question.
*Parameters*
------------
user: str
The id of the user who answered.
channel: str
The id of the channel the game is in.
command: str
The user's answer.
*Returns*
---------
send_message: str
The message to send if the function failed.
"""
triviaQuestions = self.bot.database["trivia questions"]
question = triviaQuestions.find_one({"_id": channel})
if command not in ["a", "b", "c", "d"]:
self.bot.log("I didn't quite understand that")
return "I didn't quite understand that"
elif question is None:
self.bot.log("There's no question right now")
return "There's no question right now"
elif user in question["players"]:
self.bot.log(f"{user} has already answered this question")
return f"{user} has already answered this question"
else:
self.bot.log(f"{user} answered the question in {channel}")
updater = {"$set": {f"players.{user}": command}}
triviaQuestions.update_one({"_id": channel}, updater)
return None
def triviaCountPoints(self, channel: str):
"""
Add money to every winner's account.
*Parameters*
------------
channel: str
The id of the channel the game is in.
"""
triviaQuestions = self.bot.database["trivia questions"]
question = triviaQuestions.find_one({"_id": channel})
self.bot.log("Counting points for question in "+channel)
if question is not None:
for player, answer in question["players"].items():
if answer == question["answer"]:
self.bot.money.addMoney(player, 1)
else:
self.bot.log("Couldn't find the questio")
return None
async def triviaParse(self, ctx: SlashContext, answer: str):
"""
Parse a trivia command.
*Parameters*
------------
ctx: SlashContext
The context of the command.
answer: str
The answer, if any.
"""
await self.bot.defer(ctx)
channelId = str(ctx.channel_id)
if answer == "":
question, options, correctAnswer = self.triviaStart(channelId)
if options != "":
results = "**"+question+"**\n"
for x, option in enumerate(options):
results += chr(x+97) + ") "+option+"\n"
await ctx.send(results)
await asyncio.sleep(60)
self.triviaCountPoints(channelId)
delete_gameParams = ["trivia questions", channelId]
self.bot.database_funcs.delete_game(*delete_gameParams)
self.bot.log("Time's up for the trivia question", channelId)
send_message = self.bot.long_strings["Trivia time up"]
format_parameters = [chr(correctAnswer), options[correctAnswer-97]]
send_message = send_message.format(*format_parameters)
await ctx.send(send_message)
else:
await ctx.send(question, hidden=True)
elif answer in ["a", "b", "c", "d"]:
userId = f"#{ctx.author.id}"
response = self.triviaAnswer(userId, channelId, answer)
if response is None:
user_name = ctx.author.display_name
await ctx.send(f"{user_name} answered **{answer}**")
else:
await ctx.send(response)
else:
self.bot.log("I didn't understand that", channelId)
await ctx.send("I didn't understand that")

View File

@@ -0,0 +1,201 @@
"""Implementation of Wordle"""
import requests
from PIL import Image, ImageDraw
from .game_base import DatabaseGame, BaseDrawer
from gwendolyn_old.exceptions import GameNotInDatabase
IMAGE_MARGIN = 90
SQUARE_SIZE = 150
SQUARE_PADDING = 25
ROW_PADDING = 30
FONT_SIZE = 130
# COLORS
BACKGROUND_COLOR = "#2c2f33"
TEXT_COLOR = "#eee"
WRITING_BORDER_COLOR = "#999"
INACTIVE_BORDER_COLOR = "#555"
INCORRECT_COLOR = "#444"
CORRECT_COLOR = "#0a8c20"
PRESENT_COLOR = "#0f7aa8"
COLORS = [INCORRECT_COLOR, PRESENT_COLOR, CORRECT_COLOR]
class WordleGame(DatabaseGame):
"""Implementation of wordle game."""
def __init__(self, bot):
super().__init__(bot, "wordle", WordleDrawer)
self._API_url = "https://api.wordnik.com/v4/words.json/randomWords?" # pylint: disable=invalid-name
api_key = self.bot.credentials["wordnik_key"]
self._APIPARAMS = { # pylint: disable=invalid-name
"hasDictionaryDef": True,
"minCorpusCount": 5000,
"maxCorpusCount": -1,
"minDictionaryCount": 1,
"maxDictionaryCount": -1,
"limit": 1,
"api_key": api_key
}
async def start(self, ctx, letters: int):
if self._test_document(str(ctx.channel_id)):
await ctx.send("There is already a wordle game in this channel.")
self.bot.log("There was already a game going on")
return
params = self._APIPARAMS
params["minLength"] = letters
params["maxLength"] = letters
word = "-"
while "-" in word or "." in word:
response = requests.get(self._API_url, params=params)
if response.json() == []:
ctx.send("Could not find a word. Try again")
return
word = list(response.json()[0]["word"].upper())
self.bot.log(f"Found the word \"{''.join(word)}\"")
game = {"word": word, "guesses": [], "results": []}
await ctx.send("Starting a wordle game.")
await self._start_new(ctx.channel, game, delete=False)
def _get_result(self, guess: list[str], word: list[str]):
result = ["_" for _ in guess]
for i, letter in enumerate(guess):
if letter == word[i]:
result[i] = "*"
for i, letter in enumerate(guess):
if letter in word and result[i] != "*":
same_letter = guess[:i].count(letter)
same_letter += len(
[
l for j,l in
enumerate(guess[i:])
if guess[i:][j] == letter and result[i:][j] == "*"
]
)
if same_letter < word.count(letter):
result[i] = "-"
return result
async def guess(self, ctx, guess: str):
if not guess.isalpha():
await ctx.send("You can only use letters in your guess")
return
guess = list(guess.upper())
try:
game = self.access_document(str(ctx.channel_id))
except GameNotInDatabase:
await ctx.send("No game in channel")
return
if len(guess) != len(game['word']):
await ctx.send(
f"Your guess must be {len(game['word'])} letters long")
return
await ctx.send(f"Guessed {''.join(guess)}")
result = self._get_result(guess, game['word'])
updater = {
"$set": {
f"guesses.{len(game['guesses'])}": guess,
f"results.{len(game['guesses'])}": result
}
}
self._update_document(str(ctx.channel_id), updater)
if result == ["*" for _ in game['word']]:
await ctx.send("You guessed the word! Adding 15 GwendoBucks to your account")
self.bot.money.addMoney(f"#{ctx.author_id}", 15)
await self._end_game(ctx.channel)
elif len(game['guesses']) == 5:
await ctx.send(f"You used up all available guesses. The word was '{''.join(game['word'])}'")
await self._end_game(ctx.channel)
else:
print(len(game['guesses']))
await self._delete_old_image(ctx.channel)
await self._send_image(ctx.channel, delete=False)
class WordleDrawer(BaseDrawer):
def __init__(self, bot, game: WordleGame):
super().__init__(bot, game)
self.default_image = None
self.default_color = BACKGROUND_COLOR
self.default_size = (0, 0)
def _get_size(self, game: dict):
width = (
(len(game['word']) * SQUARE_SIZE) +
((len(game['word']) - 1) * SQUARE_PADDING) +
(2 * IMAGE_MARGIN)
)
height = (
(6 * SQUARE_SIZE) +
(5 * ROW_PADDING) +
(2 * IMAGE_MARGIN)
)
size = (width, height)
return size
def _draw_row(self, row, drawer, font, word: str, colors: list[str] = None,
border_color: str = None):
if colors is None:
colors = [BACKGROUND_COLOR for _ in range(len(word))]
for i, letter in enumerate(word):
y_pos = IMAGE_MARGIN + (row * (SQUARE_SIZE + ROW_PADDING))
x_pos = IMAGE_MARGIN + (i * (SQUARE_SIZE + SQUARE_PADDING))
top_left = (x_pos, y_pos)
bottom_right = (x_pos + SQUARE_SIZE, y_pos + SQUARE_SIZE)
drawer.rounded_rectangle(
(top_left,bottom_right),
10,
colors[i],
border_color,
3
)
text_pos = (
x_pos + (SQUARE_SIZE//2),
y_pos + int(SQUARE_SIZE * 0.6)
)
drawer.text(text_pos, letter, TEXT_COLOR, font=font, anchor="mm")
def _determine_colors(self, results):
return [COLORS[["_","-","*"].index(symbol)] for symbol in results]
def _draw_image(self, game: dict, image: Image.Image):
drawer = ImageDraw.Draw(image)
font = self._get_font(FONT_SIZE)
for i, guess in enumerate(game['guesses']):
colors = self._determine_colors(game['results'][i])
self._draw_row(i, drawer, font, guess, colors)
if len(game["guesses"]) < 6:
self._draw_row(
len(game['guesses']),
drawer,
font,
" "*len(game['word']),
border_color=WRITING_BORDER_COLOR
)
for i in range(5 - len(game['guesses'])):
self._draw_row(
len(game['guesses']) + i + 1,
drawer,
font, " "*len(game['word']),
border_color=INACTIVE_BORDER_COLOR
)
return super()._draw_image(game, image)

View File

@@ -0,0 +1,221 @@
import math
import json
import discord
from gwendolyn_old.utils import cap
STATS = [
"strength",
"dexterity",
"constitution",
"intelligence",
"wisdom",
"charisma"
]
def mod(statistic):
"""Calculates D&D modifier."""
modifier = math.floor((statistic-10)/2)
if modifier >= 0:
modifier = "+"+str(modifier)
return modifier
class LookupFuncs():
def __init__(self, bot):
self.bot = bot
self.saves = [
"strength_save",
"dexterity_save",
"constitution_save",
"intelligence_save",
"wisdom_save",
"charisma_save"
]
self.abilities = [
"acrobatics",
"animal_handling",
"arcana",
"athletics",
"deception",
"history",
"insight",
"intimidation",
"investigation",
"medicine",
"nature",
"perception",
"performance",
"persuasion",
"religion",
"sleight_of_hand",
"stealth",
"survival"
]
def _format_monster(self, monster):
# Looks at the information about the monster and
# returns that information in separate variables,
# allowing Gwendolyn to know where to separate
# the messages
types = monster["type"]
if monster["subtype"] != "":
types += " ("+monster["subtype"]+")"
stats = []
for stat in STATS:
value = monster[stat]
stats.append(f"**{cap(stat[:3])}:** {value} ({mod(value)})")
stats = "\t".join(stats[:3]) + "\n" + "\t".join(stats[3:])
saving_throws = []
for save in self.saves:
if save in monster:
value = monster[save]
if monster[save] >= 0:
saving_throws.append(f"{cap(save[:3])} +{value}")
else:
saving_throws.append(f"{cap(save[:3])} {value}")
if saving_throws:
saving_throws = f"\n**Saving Throws:** {', '.join(saving_throws)}"
else:
saving_throws = ""
skills = []
for skill in self.abilities:
if skill in monster:
skill_name = cap(skill.replace("_"," "))
if monster[skill] >= 0:
skills.append(f"{skill_name} +{monster[skill]}")
else:
skills.append(f"{skill_name} {monster[skill]}")
if skills:
skills = f"\n**Skills:** {', '.join(skills)}"
else:
skills = ""
vulnerabilities = monster["damage_vulnerabilities"]
if vulnerabilities != "":
vulnerabilities = "\n**Damage Vulnerabilities** "+vulnerabilities
resistances = monster["damage_resistances"]
if resistances != "":
resistances = "\n**Damage Resistances** "+resistances
immunities = monster["damage_immunities"]
if immunities != "":
immunities = "\n**Damage Immunities** "+immunities
c_immunities = monster["condition_immunities"]
if c_immunities != "":
c_immunities = "\n**Condition Immunities** "+c_immunities
special_abilities = ""
if "special_abilities" in monster:
for ability in monster["special_abilities"]:
special_abilities += "\n\n***"+ability["name"]+".*** "+ability["desc"]
act = ""
if "actions" in monster:
for action in monster["actions"]:
act += "\n\n***"+action["name"]+".*** "+action["desc"]
react = ""
if "reactions" in monster:
for reaction in monster["reactions"]:
react += "\n\n***"+reaction["name"]+".*** "+reaction["desc"]
legendaryActions = ""
if "legendary_actions" in monster:
for action in monster["legendary_actions"]:
legendaryActions += "\n\n***"+action["name"]+".*** "+action["desc"]
hit_dice = monster["hit_dice"]
dice_amount = int(monster["hit_dice"].replace("d"," ").split()[0])
con_mod = math.floor((monster['constitution']-10)/2)
if con_mod < 0:
hit_dice += f" - {abs(con_mod) * dice_amount}"
elif con_mod > 0:
hit_dice += (f" + {con_mod * dice_amount}")
new_part = "\n--------------------"
monster_type = f"*{monster['size']} {types}, {monster['alignment']}*"
basic_info = "\n**Armor Class** "+str(monster["armor_class"])+"\n**Hit Points** "+str(monster["hit_points"])+" ("+hit_dice+")\n**Speed **"+monster["speed"]+new_part+"\n"
info = (monster_type+new_part+basic_info+stats+new_part+saving_throws+skills+vulnerabilities+resistances+immunities+c_immunities+"\n**Senses** "+monster["senses"]+"\n**Languages** "+monster["languages"]+"\n**Challenge** "+monster["challenge_rating"])
monster_info = [(info, monster['name']),
(special_abilities, "Special Abilities"),
(act, "Actions"),
(react, "Reactions"),
(legendaryActions, "Legendary Actions")]
self.bot.log("Returning monster information")
return monster_info
# Looks up a monster
async def monster_func(self, ctx, query):
query = cap(query)
self.bot.log("Looking up "+query)
# 1-letter monsters don't exist
if len(query) < 2:
self.bot.log("Monster name too short")
await ctx.send("I don't know that monster...")
return
# Opens "monsters.json"
monster_file_path = "gwendolyn/resources/lookup/monsters.json"
with open(monster_file_path,"r", encoding="utf-8") as file_pointer:
data = json.load(file_pointer)
for monster in data:
if "name" in monster and str(query) == monster["name"]:
self.bot.log("Found it!")
monster_info = self._format_monster(monster)
# Sends the received information. Separates into separate messages if
# there is too much text
await ctx.send(f"Result for \"{query}\"")
for text, title in monster_info:
if text != "":
if len(text) < 2000:
em = discord.Embed(title = title, description = text, colour=0xDEADBF)
await ctx.channel.send(embed = em)
else:
index = text[:2000].rfind(".")+1
em1 = discord.Embed(title = title, description = text[:index], colour=0xDEADBF)
await ctx.channel.send(embed = em1)
em2 = discord.Embed(title = "", description = text[index+1:], colour=0xDEADBF)
await ctx.channel.send(embed = em2)
break
else:
self.bot.log("Monster not in database")
await ctx.send("I don't know that monster...")
# Looks up a spell
async def spell_func(self, ctx, query):
query = cap(query)
self.bot.log("Looking up "+query)
# Opens "spells.json"
data = json.load(open('gwendolyn/resources/lookup/spells.json', encoding = "utf8"))
if query in data:
self.bot.log("Returning spell information")
send_message = (f"***{query}***\n*{data[query]['level']} level {data[query]['school']}\nCasting Time: {data[query]['casting_time']}\nRange:{data[query]['range']}\nComponents:{data[query]['components']}\nDuration:{data[query]['duration']}*\n \n{data[query]['description']}")
else:
self.bot.log("I don't know that spell")
send_message = "I don't think that's a spell"
if len(send_message) > 2000:
await ctx.send(send_message[:2000])
await ctx.send(send_message[2000:])
else:
await ctx.send(send_message)

View File

@@ -0,0 +1,5 @@
"""Misc. functions for Gwendolyn."""
__all__ = ["Other"]
from .other import Other

View File

@@ -0,0 +1,94 @@
import random
class Generators():
def __init__(self, bot):
self.bot = bot
# Returns a list of all letter pairs in the text
def make_pairs(self, corpus):
for i in range(len(corpus)-1):
yield (corpus[i], corpus[i+1])
# Returns a list of all letter triplets in the text
def make_triplets(self, corpus):
for i in range(len(corpus)-2):
yield (corpus[i], corpus[i+1], corpus[i+2])
# Generates a random name
async def name_gen(self, ctx):
# Makes a list of all names from "names.txt"
with open("gwendolyn/resources/names.txt", "r", encoding='utf8') as file_pointer:
names = file_pointer.read()
corpus = list(names)
# Makes a list of pairs
pairs = self.make_pairs(corpus)
triplets = self.make_triplets(corpus)
letter_dict = {}
# Makes a dictionary of all letters that come after all other letters
for letter_1, letter_2 in pairs:
if letter_1 in letter_dict:
letter_dict[letter_1].append(letter_2)
else:
letter_dict[letter_1] = [letter_2]
for letter_1, letter_2, letter_3 in triplets:
if letter_1+letter_2 in letter_dict:
letter_dict[letter_1+letter_2].append(letter_3)
else:
letter_dict[letter_1+letter_2] = [letter_3]
# Choses a random first letter
first_letter = random.choice(corpus)
# Makes sure the first letter is not something a name can't start with.
while first_letter.islower() or first_letter == " " or first_letter == "-" or first_letter == "\n":
first_letter = random.choice(corpus)
# Starts the name
chain = [first_letter]
# Picks second letter
second_letter = random.choice(letter_dict[chain[-1]])
while second_letter == "\n":
second_letter = random.choice(letter_dict[chain[-1]])
chain.append(second_letter)
done = False
# Creates the name one letter at a time
while not done:
if random.randint(1,10) > 1:
try:
new_letter = random.choice(letter_dict[chain[-2]+chain[-1]])
except KeyError():
new_letter = random.choice(letter_dict[chain[-1]])
else:
new_letter = random.choice(letter_dict[chain[-1]])
chain.append(new_letter)
# Ends name if the name ends
if new_letter == "\n":
done = True
gen_name = "".join(chain)
self.bot.log("Generated "+gen_name[:-1])
# Returns the name
await ctx.send(gen_name)
# Generates a random tavern name
async def tavern_gen(self, ctx):
# _lists first parts, second parts and third parts of tavern names
first_part = ["The Silver","The Golden","The Staggering","The Laughing","The Prancing","The Gilded","The Running","The Howling","The Slaughtered","The Leering","The Drunken","The Leaping","The Roaring","The Frowning","The Lonely","The Wandering","The Mysterious","The Barking","The Black","The Gleaming","The Tap-Dancing","The Sad","The Sexy","The Artificial","The Groovy","The Merciful","The Confused","The Pouting","The Horny","The Okay","The Friendly","The Hungry","The Handicapped","The Fire-breathing","The One-Eyed","The Psychotic","The Mad","The Evil","The Idiotic","The Trusty","The Busty"]
second_part = ["Eel","Dolphin","Dwarf","Pegasus","Pony","Rose","Stag","Wolf","Lamb","Demon","Goat","Spirit","Horde","Jester","Mountain","Eagle","Satyr","Dog","Spider","Star","Dad","Rat","Jeremy","Mouse","Unicorn","Pearl","Ant","Crab","Penguin","Octopus","Lawyer","Ghost","Toad","Handjob","Immigrant","SJW","Dragon","Bard","Sphinx","Soldier","Salmon","Owlbear","Kite","Frost Giant","Arsonist"]
third_part = [" Tavern"," Inn","","","","","","","","",""]
# Picks one of each
gen_tav = random.choice(first_part)+" "+random.choice(second_part)+random.choice(third_part)
self.bot.log("Generated "+gen_tav)
# Return the name
await ctx.send(gen_tav)

View File

@@ -0,0 +1,85 @@
import os
import requests
import discord
import wolframalpha
from PIL import Image, ImageDraw, ImageFont
class NerdShit():
def __init__(self, bot):
"""Runs misc commands."""
self.bot = bot
async def wolf_search(self,ctx,content):
await self.bot.defer(ctx)
font = ImageFont.truetype('gwendolyn/resources/fonts/times-new-roman.ttf', 20)
self.bot.log("Requesting data")
bot = wolframalpha.Client(self.bot.credentials["wolfram_alpha_key"])
res = bot.query(content)
self.bot.log("Processing data")
titles = []
pods = []
if int(res.numpods) > 0:
for pod in res.pods:
titles += [pod.title]
for i, sub in enumerate(pod.subpods):
pods += [sub]
if i > 0:
titles += [""]
pod_chunks = [pods[x:x+2] for x in range(0, len(pods), 2)]
title_chunks = [titles[x:x+2] for x in range(0, len(titles), 2)]
await ctx.send(f"Response for \"{content}\"")
for i, chunk in enumerate(pod_chunks):
width = 0
for title in title_chunks[i]:
width = max(width,font.getsize(title)[0])
height = 5
heights = []
for count, pod in enumerate(chunk):
heights += [height]
width = max(width,int(pod.img['@width']))
if title_chunks[i][count] == "":
place_for_text = 0
else:
place_for_text = 30
height += int(pod.img["@height"]) + 10 + place_for_text
width += 10
height += 5
wolf_image = Image.new("RGB",(width,height),color=(255,255,255))
for count, pod in enumerate(chunk):
response = requests.get(pod.img["@src"])
file = open("gwendolyn/resources/wolfTemp.png", "wb")
file.write(response.content)
file.close()
old_image = Image.open("gwendolyn/resources/wolfTemp.png")
old_size = old_image.size
if title_chunks[i][count] == "":
place_for_text = 0
else:
place_for_text = 30
new_size = (width,int(old_size[1]+10+place_for_text))
new_image = Image.new("RGB",new_size,color=(255,255,255))
new_image.paste(old_image, (int((int(old_size[0]+10)-old_size[0])/2),int(((new_size[1]-place_for_text)-old_size[1])/2)+place_for_text))
if title_chunks[i][count] != "":
drawer = ImageDraw.Draw(new_image,"RGB")
drawer.text((5,7),title_chunks[i][count],font=font,fill=(150,150,150))
wolf_image.paste(new_image,(0,heights[count]))
new_image.close()
old_image.close()
count += 1
wolf_image.save("gwendolyn/resources/wolf.png")
wolf_image.close()
await ctx.channel.send(file = discord.File("gwendolyn/resources/wolf.png"))
os.remove("gwendolyn/resources/wolf.png")
os.remove("gwendolyn/resources/wolfTemp.png")
else:
self.bot.log("No returned data")
await ctx.send("Could not find anything relating to your search")

View File

@@ -0,0 +1,192 @@
import random # Used in movie_func
import datetime # Used in hello_func
import urllib # Used in image_func
import ast
import imdb # Used in movie_func
import discord # Used in movie_func
import lxml # Used in image_func
import fandom # Used in find_wiki_page
import d20 # Used in roll_dice
from .plex import Plex
from .nerd_shit import NerdShit
from .generators import Generators
fandom.set_lang("da")
fandom.set_wiki("senkulpa")
class MyStringifier(d20.MarkdownStringifier):
def _str_expression(self, node):
if node.comment is None:
result_text = "Result"
else:
result_text = node.comment.capitalize()
return f"**{result_text}**: {self._stringify(node.roll)}\n**Total**: {int(node.total)}"
class Other():
def __init__(self, bot):
self.bot = bot
self.plex = Plex(self.bot)
self.nerd_shit = NerdShit(self.bot)
self.generators = Generators(self.bot)
# Picks a random movie and returns information about it
async def movie_func(self, ctx):
await self.bot.defer(ctx)
self.bot.log("Creating IMDb object")
imdb_client = imdb.IMDb()
self.bot.log("Picking a movie")
with open("gwendolyn/resources/movies.txt", "r") as file_pointer:
movie_list = file_pointer.read().split("\n")
movie_name = random.choice(movie_list)
self.bot.log(f"Searching for {movie_name}")
search_result = imdb_client.search_movie(movie_name)
self.bot.log("Getting the data")
movie = search_result[0]
imdb_client.update(movie)
self.bot.log("Successfully ran /movie")
title = movie["title"]
plot = movie['plot'][0].split("::")[0]
cover = movie['cover url'].replace("150","600").replace("101","404")
cast = ", ".join([i["name"] for i in movie['cast'][:5]])
embed = discord.Embed(title=title, description=plot, color=0x24ec19)
embed.set_thumbnail(url=cover)
embed.add_field(name="Cast", value=cast,inline = True)
await ctx.send(embed = embed)
# Responds with a greeting of a time-appropriate maner
async def hello_func(self, ctx):
def time_in_range(start, end, i):
# Return true if i is in the range [start, end]
if start <= end:
return start <= i <= end
else:
return start <= i or i <= end
author = ctx.author.display_name
now = datetime.datetime.now()
if time_in_range(now.replace(hour=5, minute=0, second=0, microsecond=0),now.replace(hour=10, minute=0, second=0, microsecond=0), now):
send_message = "Good morning, "+str(author)
elif time_in_range(now.replace(hour=13, minute=0, second=0, microsecond=0),now.replace(hour=18, minute=0, second=0, microsecond=0), now):
send_message = "Good afternoon, "+str(author)
elif time_in_range(now.replace(hour=18, minute=0, second=0, microsecond=0),now.replace(hour=22, minute=0, second=0, microsecond=0), now):
send_message = "Good evening, "+str(author)
elif time_in_range(now.replace(hour=22, minute=0, second=0, microsecond=0),now.replace(hour=23, minute=59, second=59, microsecond=0), now):
send_message = "Good night, "+str(author)
else:
send_message = "Hello, "+str(author)
await ctx.send(send_message)
# Finds a random picture online
async def image_func(self, ctx):
# Picks a type of camera, which decides the naming scheme
cams = ("one","two","three","four")
cam = random.choice(cams)
self.bot.log("Chose cam type "+cam)
if cam == "one":
search = "img_" + ''.join(
[str(random.randint(0,9)) for _ in range(4)]
)
elif cam == "two":
year = str(random.randint(2012,2016))
month = str(random.randint(1,12)).zfill(2)
day = str(random.randint(1,29)).zfill(2)
search = f"IMG_{year}{month}{day}"
elif cam == "three":
search = f"IMAG_{str(random.randint(1,500)).zfill(4)}"
elif cam == "four":
search = "DSC_" + ''.join(
[str(random.randint(0,9)) for _ in range(4)]
)
self.bot.log("Searching for "+search)
# Searches for the image and reads the resulting web page
page = urllib.request.urlopen("https://www.bing.com/images/search?q="+search+"&safesearch=off")
read = page.read()
tree = lxml.etree.HTML(read)
images = tree.xpath('//a[@class = "iusc"]/@m')
if len(images) == 0:
await ctx.send("Found no images")
else:
# Picks an image
number = random.randint(1,len(images))-1
image = ast.literal_eval(str(images[number]))
image_url = image["murl"]
self.bot.log("Picked image number "+str(number))
# Returns the image
self.bot.log("Successfully returned an image")
await ctx.send(image_url)
# Finds a page from the Senkulpa Wikia
async def find_wiki_page(self, ctx, search : str):
await self.bot.defer(ctx)
found_page = False
if search != "":
self.bot.log("Trying to find wiki page for "+search)
search_results = fandom.search(search)
if len(search_results) > 0:
found_page = True
search_result = search_results[0]
else:
self.bot.log("Couldn't find the page")
await ctx.send("Couldn't find page")
else:
found_page = True
self.bot.log("Searching for a random page")
search_result = fandom.random()
if found_page:
self.bot.log(f"Found page \"{search_result[0]}\"")
page = fandom.page(pageid = search_result[1])
content = page.summary
images = page.images
if len(images) > 0:
image = images[0]
else:
image = ""
self.bot.log("Sending the embedded message",str(ctx.channel_id))
content += f"\n[Læs mere]({page.url})"
embed = discord.Embed(title = page.title, description = content, colour=0xDEADBF)
if image != "":
embed.set_thumbnail(url=image)
await ctx.send(embed = embed)
async def roll_dice(self, ctx, roll_string):
user = ctx.author.display_name
while len(roll_string) > 1 and roll_string[0] == " ":
roll_string = roll_string[1:]
roll = d20.roll(roll_string, allow_comments=True, stringifier=MyStringifier())
await ctx.send(f"{user} :game_die:\n{roll}")
async def help_func(self, ctx, command):
if command == "":
with open("gwendolyn/resources/help/help.txt",encoding="utf-8") as file_pointer:
text = file_pointer.read()
embed = discord.Embed(title = "Help", description = text,colour = 0x59f442)
await ctx.send(embed = embed)
else:
self.bot.log(f"Looking for help-{command}.txt",str(ctx.channel_id))
with open(f"gwendolyn/resources/help/help-{command}.txt",encoding="utf-8") as file_pointer:
text = file_pointer.read()
embed = discord.Embed(title = command.capitalize(), description = text,colour = 0x59f442)
await ctx.send(embed = embed)

View File

@@ -0,0 +1,600 @@
"""Plex integration with the bot."""
from math import floor, ceil
import time
import asyncio
import requests
import imdb
import discord
import xmltodict
from interactions.utils.manage_components import (create_button,
create_actionrow)
from interactions.model import ButtonStyle
from gwendolyn_old.utils import encode_id
class Plex():
"""Container for Plex functions and commands."""
def __init__(self,bot):
self.bot = bot
self.credentials = self.bot.credentials
self.long_strings = self.bot.long_strings
server_ip = ["localhost", "192.168.0.40"][self.bot.options["testing"]]
self.radarr_url = "http://"+server_ip+":7878/api/v3/"
self.sonarr_url = "http://"+server_ip+":8989/api/"
self.qbittorrent_url = "http://"+server_ip+":8080/api/v2/"
self.movie_path = "/media/plex/Server/movies/"
self.show_path = "/media/plex/Server/Shows/"
async def request_movie(self, ctx, movie_name):
"""Request a movie for the Plex Server"""
await self.bot.defer(ctx)
self.bot.log("Searching for "+movie_name)
movie_list = imdb.IMDb().search_movie(movie_name)
movies = []
for movie in movie_list:
if movie["kind"] == "movie":
movies.append(movie)
if len(movies) > 5:
movies = movies[:5]
if len(movies) == 1:
message_title = "**Is it this movie?**"
else:
message_title = "**Is it any of these movies?**"
message_text = ""
imdb_ids = []
for i, movie in enumerate(movies):
try:
message_text += "\n"+str(i+1)+") "+movie["title"]
try:
message_text += " ("+str(movie["year"])+")"
except KeyError:
self.bot.log(f"{movie['title']} has no year.")
except KeyError:
message_text += "Error"
imdb_ids.append(movie.movieID)
self.bot.log(
f"Returning a list of {len(movies)} possible movies: {imdb_ids}"
)
embed = discord.Embed(
title=message_title,
description=message_text,
colour=0x00FF00
)
buttons = []
if len(movies) == 1:
buttons.append(create_button(
style=ButtonStyle.green,
label="",
custom_id=encode_id(["plex", "movie", str(imdb_ids[0])])
)
)
else:
for i in range(len(movies)):
buttons.append(
create_button(
style=ButtonStyle.blue,
label=str(i+1),
custom_id=encode_id(["plex", "movie", str(imdb_ids[i])])
)
)
buttons.append(create_button(
style=ButtonStyle.red,
label="X",
custom_id=encode_id(["plex", "movie", "x"])
)
)
action_rows = []
for i in range(((len(buttons)-1)//5)+1):
action_rows.append(
create_actionrow(
*buttons[(i*5):(min(len(buttons),i*5+5))]
)
)
await ctx.send(embed=embed, components=action_rows)
async def add_movie(self, message, imdb_id, edit_message = True):
"""Add a movie to Plex server."""
if not edit_message:
await message.delete()
if imdb_id == "X":
self.bot.log("Did not find what the user was searching for")
message_text = "Try searching for the IMDB id"
else:
self.bot.log("Trying to add movie "+str(imdb_id))
# Searches for the movie using the imdb id through Radarr
api_key = self.credentials["radarr_key"]
request_url = self.radarr_url+"movie/lookup/imdb?imdbId=tt"+imdb_id
request_url += "&apiKey="+api_key
response = requests.get(request_url)
# Makes the dict used for the post request
lookup_data = response.json()
post_data = {
"qualityProfileId": 1,
"rootFolderPath" : self.movie_path,
"monitored" : True,
"addOptions": {"searchForMovie": True}
}
for key in ["tmdbId","title","titleSlug","images","year"]:
post_data.update({key : lookup_data[key]})
# Makes the post request
response = requests.post(
url = self.radarr_url+"movie?apikey="+api_key,
json = post_data
)
# Deciphers the response
if response.status_code == 201:
self.bot.log("Added "+post_data["title"]+" to Plex")
message_text = "{} successfully added to Plex".format(
post_data["title"]
)
elif response.status_code == 400:
self.bot.log("The movie was already on plex")
message_text = self.long_strings["Already on Plex"].format(
post_data['title']
)
else:
self.bot.log(str(response.status_code)+" "+response.reason)
message_text = "Something went wrong",
if edit_message:
await message.edit(
embed = None,
content = message_text,
components = []
)
else:
await message.channel.send(message_text)
async def request_show(self, ctx, show_name):
"""Request a show for the Plex server."""
await self.bot.defer(ctx)
self.bot.log("Searching for "+show_name)
movies = imdb.IMDb().search_movie(show_name) # Replace with tvdb
shows = []
for movie in movies:
if movie["kind"] in ["tv series","tv miniseries"]:
shows.append(movie)
if len(shows) > 5:
shows = shows[:5]
if len(shows) == 1:
message_title = "**Is it this show?**"
else:
message_title = "**Is it any of these shows?**"
message_text = ""
imdb_ids = []
for i, show in enumerate(shows):
try:
message_text += f"\n{i+1}) {show['title']} ({show['year']})"
except KeyError:
try:
message_text += "\n"+str(i+1)+") "+show["title"]
except KeyError:
message_text += "Error"
imdb_ids.append(show.movieID)
self.bot.log(
f"Returning a list of {len(shows)} possible shows: {imdb_ids}"
)
embed = discord.Embed(
title=message_title,
description=message_text,
colour=0x00FF00
)
buttons = []
if len(shows) == 1:
buttons.append(create_button(
style=ButtonStyle.green,
label="",
custom_id=encode_id(["plex", "show", str(imdb_ids[0])])
)
)
else:
for i in range(len(shows)):
buttons.append(
create_button(
style=ButtonStyle.blue,
label=str(i+1),
custom_id=encode_id(["plex", "show", str(imdb_ids[i])])
)
)
buttons.append(create_button(
style=ButtonStyle.red,
label="X",
custom_id=encode_id(["plex", "show", "x"])
)
)
action_rows = []
for i in range(((len(buttons)-1)//5)+1):
action_rows.append(
create_actionrow(
*buttons[(i*5):(min(len(buttons),i*5+5))]
)
)
await ctx.send(embed=embed, components=action_rows)
async def add_show(self, message, imdb_id, edit_message = True):
"""Add the requested show to Plex."""
if imdb_id == "X":
self.bot.log("Did not find what the user was searching for")
message_text = "Try searching for the IMDB id"
else:
self.bot.log("Trying to add show "+str(imdb_id))
# Finds the tvdb id
tvdb_api_url = "https://thetvdb.com/api/"
tvdb_method = "GetSeriesByRemoteID.php"
tvdb_request_url = f"{tvdb_api_url}{tvdb_method}"
tvdb_id = xmltodict.parse(
requests.get(
tvdb_request_url+f"?imdbid=tt{imdb_id}",
headers = {"ContentType" : "application/json"}
).text
)['Data']['Series']['seriesid']
# Finds the rest of the information using Sonarr
api_key = self.credentials["sonarr_key"]
request_url = self.sonarr_url+"series/lookup?term="
request_url += f"tvdb:{tvdb_id}"
request_url += "&apiKey="+api_key
response = requests.get(request_url)
# Makes the dict used for the post request
lookup_data = response.json()[0]
post_data = {
"ProfileId" : 1,
"rootFolder_path" : self.show_path,
"monitored" : True,
"addOptions" : {"searchForMissingEpisodes" : True}
}
for key in ["tvdbId","title","titleSlug","images","seasons"]:
post_data.update({key : lookup_data[key]})
# Makes the post request
response = requests.post(
url= self.sonarr_url+"series?apikey="+api_key,
json = post_data
)
# Deciphers the response
if response.status_code == 201:
self.bot.log("Added a "+post_data["title"]+" to Plex")
message_text = post_data["title"]+" successfully added to Plex"
elif response.status_code == 400:
message_text = self.long_strings["Already on Plex"].format(
post_data['title']
)
else:
self.bot.log(str(response.status_code)+" "+response.reason)
message_text = "Something went wrong"
if edit_message:
await message.edit(
embed = None,
content = message_text,
components = []
)
else:
await message.channel.send(message_text)
async def __generate_download_list(self, show_dm, show_movies, show_shows,
episodes):
"""Generate a list of all torrents.
*Returns*
message_text: str
A formatted list of all torrents
all_downloaded: bool
Whether all torrents are downloaded
"""
self.bot.log("Generating torrent list")
title_width = 100
message = []
all_downloaded = True
if show_dm:
message.append("")
dm_section_title = "*Torrent Downloads*"
dm_section_title_line = "-"*((title_width-len(dm_section_title))//2)
message.append(
dm_section_title_line+dm_section_title+dm_section_title_line
)
login_url = self.qbittorrent_url+"auth/login"
username = self.credentials["qbittorrent_username"]
password = self.credentials["qbittorrent_password"]
login_url += f"?username={username}&password={password}"
cookie = {"SID": requests.get(login_url).cookies.values()[0]}
response = requests.get(
self.qbittorrent_url+"torrents/info",
cookies=cookie
)
torrent_list = response.json()
if len(torrent_list) > 0:
for torrent in torrent_list:
if torrent['category'] not in ["radarr", "tv-sonarr"]:
break
torrent_name = torrent["name"]
if len(torrent_name) > 30:
if torrent_name[26] == " ":
torrent_name = torrent_name[:26]+"...."
else:
torrent_name = torrent_name[:27]+"..."
while len(torrent_name) < 30:
torrent_name += " "
if torrent["size"] == 0:
download_ratio = 0
elif torrent["amount_left"] == 0:
download_ratio = 1
else:
download_ratio = min(
torrent["downloaded"]/torrent["size"],
1
)
progress_bar = "|"+(""*floor(download_ratio*20))
while len(progress_bar) < 21:
progress_bar += " "
progress_bar += "| "+str(floor(download_ratio*100))+"%"
while len(progress_bar) < 27:
progress_bar += " "
eta_in_seconds = torrent["eta"]
if eta_in_seconds >= 8640000:
eta = ""
else:
eta = ""
if eta_in_seconds >= 86400:
eta += str(floor(eta_in_seconds/86400))+"d "
if eta_in_seconds >= 3600:
eta += str(floor((eta_in_seconds%86400)/3600))+"h "
if eta_in_seconds >= 60:
eta += str(floor((eta_in_seconds%3600)/60))+"m "
eta += str(eta_in_seconds%60)+"s"
torrent_info = f"{torrent_name} {progress_bar} "
torrent_info += f"(Eta: {eta})"
if torrent["state"] == "stalledDL":
torrent_info += " (Stalled)"
if not (download_ratio == 1 and
torrent["last_activity"] < time.time()-7200):
message.append(torrent_info)
if download_ratio < 1 and torrent["state"] != "stalledDL":
all_downloaded = False
else:
message.append("No torrents currently downloading")
if show_movies:
message.append("")
movies_section_title = "*Missing movies not downloading*"
movies_section_line = (
"-"*((title_width-len(movies_section_title))//2)
)
message.append(
movies_section_line+movies_section_title+movies_section_line
)
movie_list = requests.get(
self.radarr_url+"movie?apiKey="+self.credentials["radarr_key"]
).json()
print(
self.radarr_url+"movie?apiKey="+self.credentials["radarr_key"]
)
movie_queue = requests.get(
self.radarr_url+"queue?apiKey="+self.credentials["radarr_key"]
).json()
movie_queue_ids = []
for queue_item in movie_queue["records"]:
movie_queue_ids.append(queue_item["movieId"])
for movie in movie_list:
if (not movie["hasFile"] and
movie["id"] not in movie_queue_ids):
movie_name = movie["title"]
if len(movie_name) > 40:
if movie_name[36] == " ":
movie_name = movie_name[:36]+"...."
else:
movie_name = movie_name[:37]+"..."
while len(movie_name) < 41:
movie_name += " "
if movie["monitored"]:
movie_info = movie_name+"Could not find a torrent"
else:
movie_info = self.long_strings["No torrent"].format(
movie_name
)
message.append(movie_info)
if show_shows:
message.append("")
show_section_title = "*Missing shows not downloading*"
show_section_line = "-"*((title_width-len(show_section_title))//2)
message.append(
show_section_line+show_section_title+show_section_line
)
show_list = requests.get(
self.sonarr_url+"series?apiKey="+self.credentials["sonarr_key"]
).json()
for show in show_list:
if show["seasons"][0]["seasonNumber"] == 0:
seasons = show["seasons"][1:]
else:
seasons = show["seasons"]
if any(
(
i["statistics"]["episodeCount"] !=
i["statistics"]["totalEpisodeCount"]
) for i in seasons):
if all(
i["statistics"]["episodeCount"] == 0 for i in seasons
):
message.append(show["title"] + " (all episodes)")
else:
if episodes:
missing_episodes = sum(
(i["statistics"]["totalEpisodeCount"] -
i["statistics"]["episodeCount"])
for i in seasons)
message.append(
f"{show['title']} ({missing_episodes} episodes)"
)
message.append("-"*title_width)
message_text = "```"+"\n".join(message[1:])+"```"
if message_text == "``````":
message_text = self.long_strings["No torrents downloading"]
return message_text, all_downloaded
async def downloading(self, ctx, content):
"""Send message with list of all downloading torrents."""
async def send_long_message(ctx,message_text):
if len(message_text) <= 1994:
await ctx.send("```"+message_text+"```")
else:
cut_off_index = message_text[:1994].rfind("\n")
await ctx.send("```"+message_text[:cut_off_index]+"```")
await send_long_message(ctx,message_text[cut_off_index+1:])
await self.bot.defer(ctx)
# showDM, showMovies, showShows, episodes
parameters = [False, False, False, False]
show_dm_args = ["d", "dm", "downloading", "downloadmanager"]
show_movies_args = ["m", "movies"]
show_shows_args = ["s", "shows", "series"]
show_episode_args = ["e", "episodes"]
arg_list = [
show_dm_args, show_movies_args, show_shows_args, show_episode_args
]
input_args = []
valid_arguments = True
while content != "" and valid_arguments:
if content[0] == " ":
content = content[1:]
elif content[0] == "-":
if content[1] == "-":
arg_start = 2
if " " in content:
arg_stop = content.find(" ")
else:
arg_stop = None
else:
arg_start = 1
arg_stop = 2
input_args.append(content[arg_start:arg_stop])
if arg_stop is None:
content = ""
else:
content = content[arg_stop:]
else:
valid_arguments = False
if valid_arguments:
for arg_index, arg_aliases in enumerate(arg_list):
arg_in_input = [i in input_args for i in arg_aliases]
if any(arg_in_input):
input_args.remove(arg_aliases[arg_in_input.index(True)])
parameters[arg_index] = True
if len(input_args) != 0 or (not parameters[2] and parameters[3]):
valid_arguments = False
show_anything = any(i for i in parameters)
if not (valid_arguments and show_anything):
await ctx.send(self.long_strings["Invalid parameters"])
else:
message_text, all_downloaded = await self.__generate_download_list(
*parameters
)
if not message_text.startswith("```"):
await ctx.send(message_text)
elif len(message_text) > 2000:
message_text = message_text[3:-3]
await send_long_message(ctx,message_text)
elif all_downloaded:
await ctx.send(message_text)
else:
updates_left = 60
message_text = self.long_strings["Update"].format(
message_text[:-3], ceil(updates_left/6)
)
old_message = await ctx.send(message_text)
while ((not all_downloaded) and updates_left > 0):
await asyncio.sleep(10)
updates_left -= 1
message_text, all_downloaded = await (
self.__generate_download_list(*parameters)
)
message_text = self.long_strings["Update"].format(
message_text[:-3],
ceil(updates_left/6)
)
await old_message.edit(content = message_text)
message_text, all_downloaded = await (
self.__generate_download_list(*parameters)
)
if message_text.startswith("```"):
if all_downloaded:
self.bot.log("All torrents are downloaded")
else:
message_text = self.long_strings["No updates"].format(
message_text[:-3]
)
self.bot.log("The message updated 20 times")
await old_message.edit(content = message_text)

View File

@@ -0,0 +1,10 @@
from .star_wars_char import StarWarsChar
from .star_wars_roll import StarWarsRoll
from .star_wars_destiny import StarWarsDestiny
class StarWars():
def __init__(self, bot):
self.bot = bot
self.character = StarWarsChar(self.bot)
self.roll = StarWarsRoll(self.bot)
self.destiny = StarWarsDestiny(self.bot)

View File

@@ -0,0 +1,528 @@
import json
import string
import discord
class StarWarsChar():
def __init__(self, bot):
self.bot = bot
def get_char_name(self, user : str):
self.bot.log("Getting name for "+self.bot.database_funcs.get_name(user)+"'s character")
user_character = self.bot.database["starwars characters"].find_one({"_id":user})
if user_character != None:
self.bot.log("Name is "+user_character["Name"])
return user_character["Name"]
else:
self.bot.log("Just using "+self.bot.database_funcs.get_name(user))
return self.bot.database_funcs.get_name(user)
def set_up_dict(self, cmd : dict):
self.bot.log("Setting up a dictionary in a nice way")
if bool(cmd):
keys = list(cmd)
values = list(cmd.values())
result = ""
if isinstance(values[0],dict):
return ", ".join(values)
else:
for i, key in enumerate(keys):
if type(key) is list:
if i%3 != 2:
result += "**" + key + "**" + ": " + ", ".join(values[i]) + " "
else:
result += "**" + key + "**" + ": " + ", ".join(values[i]) + "\n"
else:
if i%3 != 2:
result += "**" + key + "**" + ": " + str(values[i]) + " "
else:
result += "**" + key + "**" + ": " + str(values[i]) + "\n"
self.bot.log("Returning a dictionary, but well formatted")
return result
else:
self.bot.log("Couldn't find anything")
return "There doesn't seem to be anything here..."
def look_up(self, data : dict, key : str, cmd : str = ""):
if cmd == " ":
cmd = ""
elif cmd != "":
while cmd[0] == " ":
cmd = cmd[1:]
if cmd == "":
break
self.bot.log("Looking up "+key)
if key in data:
self.bot.log(key+" exists")
if cmd == "":
if type(data[key]) is dict and key != "Weapons":
return self.set_up_dict(data[key])
elif key == "Weapons":
self.bot.log("Does this even get used? I'm too scared to delete it")
if bool(data[key]):
self.bot.log("Found "+(", ".join(list(data[key]))))
return ", ".join(list(data[key]))
else:
self.bot.log("There is nothing here")
return "There doesn't seem to be anything here..."
else:
if str(data[key]) != "":
self.bot.log("Returning "+str(data[key]))
return data[key]
else:
self.bot.log("There was nothing there")
return "There doesn't seem to be anything here"
elif cmd[0] == '+':
self.bot.log("Trying to add to "+key)
try:
cmd = cmd[1:]
while cmd[0] == ' ':
cmd = cmd[1:]
except:
self.bot.log("Yeah, that fucked up")
return "Can't do that"
if type(data[key]) is int:
try:
new_value = data[key] + int(cmd)
data[key] = new_value
self.bot.log("Added "+cmd+" to "+key)
return data
except:
self.bot.log("Couldn't add "+cmd+" to "+key)
return "Can't add that"
elif type(data[key]) is list:
try:
data[key].append(cmd)
self.bot.log("Added "+cmd+" to "+key)
return data
except:
self.bot.log("Couldn't add "+cmd+" to "+key)
return "Can't add that"
else:
self.bot.log("Yeah, I can't add that to "+key)
return "Can't add that"
elif cmd[0] == '-':
self.bot.log("Trying to remove/subtract from "+key)
try:
cmd = cmd[1:]
while cmd[0] == ' ':
cmd = cmd[1:]
except:
self.bot.log("Yeah, that fucked up")
return "Can't do that"
if type(data[key]) is int:
try:
new_value = data[key] - int(cmd)
data[key] = new_value
self.bot.log("Subtracted "+cmd+" from "+key)
return data
except:
self.bot.log("Couldn't subtract "+cmd+" from "+key)
return "Can't remove that"
elif type(data[key]) is list:
try:
data[key].remove(cmd)
self.bot.log("Removed "+cmd+" from "+key)
return data
except:
self.bot.log("Couldn't remove "+cmd+" from "+key)
return "Can't remove that"
else:
self.bot.log("Yeah, I can't remove/subtract that from "+key)
return "Can't remove that"
else:
while cmd[0] == ' ':
cmd = cmd[1:]
if type(data[key]) is dict:
new_key = cmd.split(" ")[0]
cmd = cmd[len(new_key):]
if cmd != "":
while cmd[0] == " ":
cmd = cmd[1:]
if cmd == "":
break
self.bot.log("Looking up "+new_key+" in "+key)
look_up_result = self.look_up(data[key],new_key,cmd)
if type(look_up_result) is dict:
data[key] = look_up_result
return data
else:
return look_up_result
elif type(data[key]) != list:
self.bot.log("Trying to change "+key+" to "+cmd)
try:
cmd = type(data[key])(cmd)
data[key] = cmd
self.bot.log("Did that")
return data
except:
self.bot.log("No. That did not work")
return "Wrong data type"
else:
self.bot.log("Yeah, that didn't work")
return "Wrong data type"
else:
self.bot.log("Couldn't find "+key)
self.bot.log("Testing to see if it's a multi-word key")
cmd = key + " " + cmd
words = cmd.split(" ")
search = ""
i = 0
while search not in data:
try:
search += " " + words[i]
i += 1
except:
self.bot.log("It wasn't. "+search+" doesn't exist")
return search + " doesn't exist"
if search[0] == " ":
search = search[1:]
self.bot.log("Yeah! Did that! The key was "+search)
cmd = cmd[len(search):]
if cmd != "":
while cmd[0] == " ":
cmd = cmd[1:]
if cmd == "":
break
if cmd == "":
self.bot.log("Returning "+search)
return self.set_up_dict(data[search])
else:
new_key = cmd.split(" ")[0]
cmd = cmd[len(new_key):]
if cmd != "":
while cmd[0] == " ":
cmd = cmd[1:]
if cmd == "":
break
look_up_result = self.look_up(data[search],new_key,cmd)
if type(look_up_result) is dict:
data[search] = look_up_result
return data
else:
return look_up_result
def character_sheet(self,character : dict):
self.bot.log("Setting up a character sheet for "+character["Name"])
divider = "--------------------\n"
name = character["Name"]
textf = ""
if character["Force-rating"] != 0:
textf = "\n**Force Rating**: "+str(character["Force-rating"])
text1 = "**Species**: "+character["Species"]+"\n**Career**: "+character["Career"]+"\n**Specialization Trees**: "+", ".join(character["Specialization-trees"])+textf+"\n**Soak**: "+str(character["Soak"])
text2 = "\n\n**Wounds**: "+str(character["Wounds"])+"/"+str(character["Wound-threshold"])+"\n**Strain**: "+str(character["Strain"])+"/"+str(character["Strain-threshold"])
text3 = self.set_up_dict(character["Characteristics"])
text4 = self.set_up_dict(character["Skills"])
text5 = ""
text6 = ""
text7 = ""
text8 = ""
if bool(character["Talents"]):
text5 = "**Talents**: "+", ".join(list(character["Talents"]))+"\n\n"
if bool(character["Force-powers"]):
text6 = "**Force Powers**: "+", ".join(list(character["Force-powers"]))+"\n\n"
text7 = "**Equipment**: "+", ".join(character["Equipment"])+"\n**Credits**: "+str(character["Credits"])+"\n**Weapons**: "+", ".join(list(character["Weapons"]))+"\n"+divider
if bool(character["Obligations"]):
text8 = "**Obligations**: "+",".join(list(character["Obligations"]))
return name, text1+text2+"\n\n"+text3+divider+text4+"\n"+divider+text5+text6+text7+text8
def char_data(self,user : str,cmd : str):
user_character = self.bot.database["starwars characters"].find_one({"_id":user})
key = string.capwords(cmd.split(" ")[0])
cmd = cmd[len(key):]
if cmd == " ":
cmd = ""
elif cmd != "":
while cmd[0] == " ":
cmd = cmd[1:]
if cmd == "":
break
self.bot.log("Looking for "+self.bot.database_funcs.get_name(user)+"'s character")
if user_character != None:
self.bot.log("Found it! Looking for "+key+" in the data")
if key in user_character:
self.bot.log("Found it!")
if type(user_character[key]) is dict:
self.bot.log("It's a dictionary!")
if cmd == "":
self.bot.log("Retrieving data")
if key == "Weapons":
if bool(user_character[key]):
self.bot.log("Returning a list of weapons")
return ", ".join(list(user_character[key]))
else:
self.bot.log("The character doesn't have any weapons. Which is probably for the best. Like, who just walks around with weapons?")
return "There doesn't seem to be anything there..."
else:
return self.set_up_dict(user_character[key])
elif cmd[0] == "+":
self.bot.log("Gonna add something!!!")
try:
cmd = cmd[1:]
while cmd[0] == ' ':
cmd = cmd[1:]
except:
self.bot.log("Nope. That didn't happen")
return "Can't do that"
if (key == "Talents" or key == "Force-powers") and "," in cmd:
cmd = cmd.split(",")
while cmd[1][0] == " ":
cmd[1] = cmd[1][1:]
self.bot.log("Adding "+cmd[0]+" to "+key)
self.bot.database["starwars characters"].update_one({"_id":user},
{"$set": {key+"."+cmd[0] : cmd[1]}})
return cmd[0]+" added to "+key+" for " + user_character["Name"]
elif key == "Obligations" and "," in cmd:
cmd = cmd.split(",")
while cmd[1][0] == " ":
cmd[1] = cmd[1][1:]
self.bot.log("Adding "+cmd[0]+" to "+key)
try:
self.bot.database["starwars characters"].update_one({"_id":user},
{"$set": {key+"."+cmd[0] : int(cmd[1])}})
except:
self.bot.log("Fucked that up")
return "Wrong data type"
return cmd[0]+" added to "+key+" for " + user_character["Name"]
elif key == "Weapons":
with open("gwendolyn/resources/star_wars/starwarstemplates.json", "r") as file_pointer:
templates = json.load(file_pointer)
new_weapon = templates["Weapon"]
self.bot.log("Adding "+cmd+" to "+key)
self.bot.database["starwars characters"].update_one({"_id":user},
{"$set": {key+"."+cmd : new_weapon}})
return cmd+" added to weapons for " + user_character["Name"]
else:
self.bot.log("That's not happening")
return "Can't add that"
elif cmd[0] == "-":
self.bot.log("Gonna subtract/remove something")
try:
cmd = cmd[1:]
while cmd[0] == ' ':
cmd = cmd[1:]
except:
self.bot.log("AAAAAAAAAAAA ")
return "Can't do that "
if key == "Talents" or key == "Force-powers" or key == "Weapons" or key == "Obligations":
self.bot.log("Trying to remove "+cmd+" from "+key)
if cmd in user_character[key]:
self.bot.database["starwars characters"].update_one({"_id":user},
{"$unset": {cmd}})
self.bot.log("I did that")
return cmd+" removed from "+key+" from "+user_character["Name"]
else:
self.bot.log("Welp. I fucked that up")
return "Can't remove that"
else:
self.bot.log("Urgh!")
return "Can't remove that"
else:
self.bot.log("Looking up "+cmd+" in "+key)
if key == "Talents" or key == "Force-powers":
new_key = cmd
new_cmd = ""
else:
new_key = string.capwords(cmd.split(" ")[0])
new_cmd = cmd[len(new_key):]
look_up_result = self.look_up(user_character[key],new_key,new_cmd)
if type(look_up_result) is dict:
self.bot.database["starwars characters"].update_one({"_id":user},
{"$set": {key : look_up_result}})
return "Changed " + user_character["Name"] + "'s " + key
else:
return look_up_result
else:
if cmd == "":
self.bot.log("Retrieving data")
if type(user_character[key]) is list:
return key+":\n"+", ".join(user_character[key])
else:
return user_character[key]
elif cmd[0] == '+':
self.bot.log("Adding")
try:
cmd = cmd[1:]
while cmd[0] == ' ':
cmd = cmd[1:]
except:
self.bot.log("Error message")
return "Can't do that"
if type(user_character[key]) is int:
try:
self.bot.log("Adding "+cmd+" to "+key)
self.bot.database["starwars characters"].update_one({"_id":user},
{"$inc": {key : int(cmd)}})
return "Added " + cmd + " to " + user_character["Name"] + "'s " + key
except:
self.bot.log("BITCH SANDWICH")
return "Can't add that"
elif type(user_character[key]) is list:
try:
self.bot.log("Adding "+cmd+" to "+key)
self.bot.database["starwars characters"].update_one({"_id":user},
{"$push": {key : cmd}})
return "Added " + cmd + " to " + user_character["Name"] + "'s " + key
except:
self.bot.log("tstststststs")
return "Can't add that"
else:
self.bot.log("Help")
return "Can't add that"
elif cmd[0] == '-':
self.bot.log("Removing/subtracting")
try:
cmd = cmd[1:]
while cmd[0] == ' ':
cmd = cmd[1:]
except:
self.bot.log("lalalala ")
return "Can't do that"
if type(user_character[key]) is int:
try:
self.bot.log("Subtracting "+cmd+" from "+key)
self.bot.database["starwars characters"].update_one({"_id":user},
{"$inc": {key : -int(cmd)}})
return "Subtracted " + cmd + " from " + user_character["Name"] + "'s " + key
except:
self.bot.log("Tried it. Didn't want to")
return "Can't remove that"
elif type(user_character[key]) is list:
try:
self.bot.log("removing "+cmd+" from "+key)
try:
self.bot.database["starwars characters"].update_one({"_id":user},
{"$pull": {key : cmd}})
except:
self.bot.log("They can only remove stuff that's actually in the list")
return "Not in list"
return "Removed " + cmd + " from " + user_character["Name"] + "'s " + key
except:
self.bot.log("nah")
return "Can't remove that"
else:
self.bot.log("nyope")
return "Can't remove that"
else:
self.bot.log("Changing "+key+" to "+cmd)
if type(user_character[key]) is int:
try:
self.bot.database["starwars characters"].update_one({"_id":user},
{"$set": {key : int(cmd)}})
except:
self.bot.log("I don't wanna tho")
return "Can't do that"
elif type(user_character[key]) is str:
self.bot.database["starwars characters"].update_one({"_id":user},
{"$set": {key : cmd}})
else:
self.bot.log("I don't wanna tho")
return "Can't do that"
return "Changed " + user_character["Name"] + "'s " + key +" to " + cmd
else:
self.bot.log(key+" isn't in there")
return "Couldn't find that data. Are you sure you spelled it correctly?"
else:
self.bot.log(user+" doesn't have a character")
return "You don't have a character. You can make one with /starwarscharacter"
def replace_spaces(self,cmd : str):
with_spaces = ["Specialization Trees","Wound Threshold","Strain Threshold","Defense - Ranged","Defense - Melee","Force Rating","Core Worlds","Outer Rim","Piloting - Planetary","Piloting - Space","Ranged - Heavy","Ranged - Light","Lightsaber Characteristic","Critical Injuries","Force Powers"]
without_spaces = ["Specialization-trees","Wound-threshold","Strain-threshold","Defense-ranged","Defense-melee","Force-rating","Core-worlds","Outer-rim","Piloting-planetary","Piloting-space","Ranged-heavy","Ranged-light","Lightsaber-characteristic","Critical-injuries","Force-powers"]
for i, value in enumerate(without_spaces):
cmd = cmd.replace(with_spaces[i],value)
return cmd
def replace_with_spaces(self,cmd : str):
with_spaces = ["Specialization Trees","Wound Threshold","Strain Threshold","Defense - Ranged","Defense - Melee","Force Rating","Core Worlds","Outer Rim","Piloting - Planetary","Piloting - Space","Ranged - Heavy","Ranged - light","Lightsaber Characteristic","Critical Injuries","Force Powers"]
without_spaces = ["Specialization-trees","Wound-threshold","Strain-threshold","Defense-ranged","Defense-melee","Force-rating","Core-worlds","Outer-rim","Piloting-planetary","Piloting-space","Ranged-heavy","Ranged-light","Lightsaber-characteristic","Critical-injuries","Force-powers"]
for i, value in enumerate(without_spaces):
cmd = cmd.replace(value,with_spaces[i])
return cmd
async def parse_char(self, ctx, parameters : str):
user = f"#{ctx.author.id}"
cmd = string.capwords(parameters.replace("+","+ ").replace("-","- ").replace(",",", "))
return_embed = False
cmd = self.replace_spaces(cmd)
user_character = self.bot.database["starwars characters"].find_one({"_id":user})
if cmd == " ":
cmd = ""
elif cmd != "":
while cmd[0] == " ":
cmd = cmd[1:]
if cmd == "":
break
if cmd == "":
if user_character != None:
title, text = self.character_sheet(user_character)
text = self.replace_with_spaces(text)
return_embed = True
else:
self.bot.log("Makin' a character for "+self.bot.database_funcs.get_name(user))
with open("gwendolyn/resources/star_wars/starwarstemplates.json", "r") as file_pointer:
templates = json.load(file_pointer)
new_char = templates["Character"]
new_char["_id"] = user
self.bot.database["starwars characters"].insert_one(new_char)
await ctx.send("Character for " + self.bot.database_funcs.get_name(user) + " created")
else:
if cmd == "Purge":
self.bot.log("Deleting "+self.bot.database_funcs.get_name(user)+"'s character")
self.bot.database["starwars characters"].delete_one({"_id":user})
await ctx.send("Character for " + self.bot.database_funcs.get_name(user) + " deleted")
else:
await ctx.send(self.replace_with_spaces(str(self.char_data(user,cmd))))
if return_embed:
embed = discord.Embed(title = title, description = text, colour=0xDEADBF)
await ctx.send(embed = embed)
def lightsaber_char(self,user : str):
user_character = self.bot.database["starwars characters"].find_one({"_id":user})
if user_character != None:
return user_character["Lightsaber-characteristic"]
def user_has_char(self,user : str):
user_character = self.bot.database["starwars characters"].find_one({"_id":user})
return user_character != None

View File

@@ -0,0 +1,72 @@
class StarWarsDestiny():
def __init__(self, bot):
self.bot = bot
def destiny_new(self, num : int):
self.bot.log("Creating a new destiny pool with "+str(num)+" players")
roll, dice_results = self.bot.star_wars.roll.roll(0,0,0,0,0,0,num)
roll = "".join(sorted(roll))
with open("gwendolyn/resources/star_wars/destinyPoints.txt","wt") as file_pointer:
file_pointer.write(roll)
return "Rolled for Destiny Points and got:\n"+self.bot.star_wars.roll.diceResultToEmoji(dice_results)+"\n"+self.bot.star_wars.roll.resultToEmoji(roll)
def destiny_use(self, user : str):
with open("gwendolyn/resources/star_wars/destinyPoints.txt","rt") as file_pointer:
points = file_pointer.read()
if user == "Nikolaj":
self.bot.log("Trying to use a dark side destiny point")
if 'B' in points:
points = points.replace("B","L",1)
points = "".join(sorted(points))
with open("gwendolyn/resources/star_wars/destinyPoints.txt","wt") as file_pointer:
file_pointer.write(points)
self.bot.log("Did it")
return "Used a dark side destiny point. Destiny pool is now:\n"+self.bot.star_wars.roll.resultToEmoji(points)
else:
self.bot.log("There were no dark side destiny points")
return "No dark side destiny points"
else:
self.bot.log("Trying to use a light side destiny point")
if 'L' in points:
points = points.replace("L","B",1)
points = "".join(sorted(points))
with open("gwendolyn/resources/star_wars/destinyPoints.txt","wt") as file_pointer:
file_pointer.write(points)
self.bot.log("Did it")
return "Used a light side destiny point. Destiny pool is now:\n"+self.bot.star_wars.roll.resultToEmoji(points)
else:
self.bot.log("There were no dark side destiny points")
return "No light side destiny points"
async def parse_destiny(self, ctx, cmd : str):
user = f"#{ctx.author.id}"
if cmd != "":
while cmd[0] == ' ':
cmd = cmd[1:]
if cmd == "":
break
if cmd == "":
self.bot.log("Retrieving destiny pool info")
with open("gwendolyn/resources/star_wars/destinyPoints.txt","rt") as file_pointer:
send_message = self.bot.star_wars.roll.resultToEmoji(file_pointer.read())
else:
commands = cmd.upper().split(" ")
if commands[0] == "N":
if len(commands) > 1:
send_message = self.destiny_new(int(commands[1]))
else:
send_message = "You need to give an amount of players"
elif commands[0] == "U":
send_message = self.destiny_use(user)
else:
send_message = "I didn't quite understand that"
message_list = send_message.split("\n")
await ctx.send(message_list[0])
if len(message_list) > 1:
for message_item in message_list[1:]:
await ctx.channel.send(message_item)

View File

@@ -0,0 +1,391 @@
import random
import re
import string
import json
with open("gwendolyn/resources/star_wars/starwarsskills.json", "r") as f:
skill_data = json.load(f)
class StarWarsRoll():
def __init__(self, bot):
self.bot = bot
# Rolls the specified dice
def roll(self, abi : int = 1, prof : int = 0, dif : int = 3, cha : int = 0, boo : int = 0, setb : int = 0, force : int = 0):
result = ""
dice_result = []
for _ in range(abi):
choice = random.choice(["","S","S","SS","A","A","SA","AA"])
result += choice
dice_result.append("abi"+choice)
for _ in range(prof):
choice = random.choice(["","S","S","SS","SS","A","SA","SA","SA","AA","AA","R"])
result += choice
dice_result.append("prof"+choice)
for _ in range(dif):
choice = random.choice(["","F","FF","H","H","H","HH","FH"])
result += choice
dice_result.append("dif"+choice)
for _ in range(cha):
choice = random.choice(["","F","F","FF","FF","H","H","FH","FH","HH","HH","D"])
result += choice
dice_result.append("cha"+choice)
for _ in range(boo):
choice = random.choice(["","","S","SA","AA","A"])
result += choice
dice_result.append("boo"+choice)
for _ in range(setb):
choice = random.choice(["","","F","F","H","H"])
result += choice
dice_result.append("setb"+choice)
for _ in range (force):
choice = random.choice(["B","B","B","B","B","B","BB","L","L","LL","LL","LL"])
result += choice
dice_result.append("force"+choice)
return result, dice_result
# Lets dice cancel each other out
def simplify(self, result : str):
self.bot.log("Simplifying "+result)
simp = ""
success = (result.count('S') + result.count('R')) - (result.count('F') + result.count('D'))
advantage = result.count('A') - result.count('H')
result = re.sub("S|A|F|H","",result)
if success > 0:
for _ in range(success):
simp += "S"
elif success < 0:
for _ in range(abs(success)):
simp += "F"
if advantage > 0:
for _ in range(advantage):
simp += "A"
elif advantage < 0:
for _ in range(abs(advantage)):
simp += "H"
simp += result
return simp
# Returns emoji that symbolize the dice results
def dice_result_to_emoji(self, dice_results : list):
emoji = ""
for result in dice_results:
if result == "abiA":
emoji += "<:abil1a:695267684476125264> "
elif result == "abiSA":
emoji += "<:abil1a1s:695267684484513842> "
elif result == "abiS":
emoji += "<:abil1s:695267684514005013> "
elif result == "abiAA":
emoji += "<:abil2a:695267684547428352> "
elif result == "abiSS":
emoji += "<:abil2s:695267684761206914> "
elif result == "abi":
emoji += "<:abilbla:695267684660674602> "
elif result == "profA":
emoji += "<:prof1a:695267685361123338> "
elif result == "profSA":
emoji += "<:prof1a1s:695267685067653140> "
elif result == "profR":
emoji += "<:prof1r:695267685067522088> "
elif result == "profS":
emoji += "<:prof1s:695267684899881012> "
elif result == "profAA":
emoji += "<:prof2a:695267684996218982> "
elif result == "profSS":
emoji += "<:prof2s:695267684878647327> "
elif result == "prof":
emoji += "<:profbla:695267684698292235> "
elif result == "difF":
emoji += "<:dif1f:695267684924915804> "
elif result == "difH":
emoji += "<:dif1h:695267684908138506> "
elif result == "difFH":
emoji += "<:dif1h1f:695267684908269678> "
elif result == "difFF":
emoji += "<:dif2f:695267684924784680> "
elif result == "difHH":
emoji += "<:dif2h:695267685071585340> "
elif result == "dif":
emoji += "<:difbla:695267685000544276> "
elif result == "chaD":
emoji += "<:cha1d:695267684962533447> "
elif result == "chaF":
emoji += "<:cha1f:695267684601954346> "
elif result == "chaH":
emoji += "<:cha1h:695267685046681620> "
elif result == "chaFH":
emoji += "<:cha1h1f:695267685063327784> "
elif result == "chaFF":
emoji += "<:cha2f:695267684832641097> "
elif result == "chaHH":
emoji += "<:cha2h:695267684631183381> "
elif result == "cha":
emoji += "<:chabla:695267684895686787> "
elif result == "booA":
emoji += "<:boo1a:695267684975116329> "
elif result == "booSA":
emoji += "<:boo1a1s:695267684970922024> "
elif result == "booS":
emoji += "<:boo1s:695267684979441714> "
elif result == "booAA":
emoji += "<:boo2a:695267685100945488> "
elif result == "boo":
emoji += "<:boobla:695267684757012550> "
elif result == "setbF":
emoji += "<:set1f:695267685054939197> "
elif result == "setbH":
emoji += "<:set1h:695267685147082802> "
elif result == "setb":
emoji += "<:setbla:695267685151408169> "
elif result == "forceB":
emoji += "<:for1b:695267684593434677> "
elif result == "forceL":
emoji += "<:for1l:695267684606148640> "
elif result == "forceBB":
emoji += "<:for2b:695267684903944303> "
elif result == "forceLL":
emoji += "<:for2l:695267684992024626> "
return emoji
# Returns emoji that symbolize the results of the dice rolls
def result_to_emoji(self, result : str):
emoji = ""
for char in result:
if char == 'S':
emoji += "<:success:826026925280854026> "
elif char == 'A':
emoji += "<:advantage:826026925515604009> "
elif char == 'R':
emoji += "<:triumph:826026925319127070> "
elif char == 'F':
emoji += "<:failure:826026925288980511> "
elif char == 'H':
emoji += "<:threat:826026925280985108> "
elif char == 'D':
emoji += "<:despair:826026925272203294> "
elif char == 'L':
emoji += "<:light:826026925059211295>"
elif char == 'B':
emoji += "<:dark:826026925289373717>"
return emoji
# Converts emoji into letters
def emoji_to_result(self, emoji : str):
result = ""
for char in emoji:
if char == "<:light:691010089905029171>":
emoji += 'L'
if char == "<:dark:691010101901000852>":
emoji += 'B'
return result
# Returns emoji that symbolize the dice
def dice_to_emoji(self, dice : list):
emoji = ""
for _ in range(dice[0]):
emoji += "<:ability:690974213397282826> "
for _ in range(dice[1]):
emoji += "<:proficiency:690973435354153071> "
for _ in range(dice[2]):
emoji += "<:difficulty:690973992470708296> "
for _ in range(dice[3]):
emoji += "<:challenge:690973419906400306> "
for _ in range(dice[4]):
emoji += "<:boost:690972178216386561> "
for _ in range(dice[5]):
emoji += "<:setback:690972157890658415> "
for _ in range(dice[6]):
emoji += "<:force:690973451883774013> "
return emoji
# Rolls for obligation
def obligation_roll(self):
self.bot.log("Rolling for obligation")
data = self.bot.database["starwarscharacters"]
table = []
for character in data:
for obligation in data[character]["Obligations"]:
for _ in range(data[character]["Obligations"][obligation]):
table.append(data[character]["Name"]+", "+obligation)
while len(table) < 100:
table.append("Nothing")
return random.choice(table)
# Rolls for critical injury
async def crit_roll(self, ctx, addington : int):
difficulty_die = "<:difficulty:690973992470708296>"
setback_die = "<:setback:690972157890658415>"
boost_die = "<:boost:690972178216386561>"
roll = random.randint(1,100) + addington
injuries = [
"**Minor nick**: The target suffers 1 strain, "+difficulty_die] * 5 + [
"**Slowed down**: The target can only act during the last allied initiative slot this turn, "+difficulty_die] * 5 + [
"**Sudden Jolt**: The target drops whatever is in hand, "+difficulty_die] * 5 + [
"**Distracted**: The target cannot perform a Free maneuver during his next turn, "+difficulty_die] * 5 + [
"**Off-Balance**: The target adds "+setback_die+" to his next skill check, "+difficulty_die] * 5 + [
"**Discouraging Wound**: Flip one light side Destiny point to a dark side Destiny point (reverse if NPC), "+difficulty_die] * 5 + [
"**Stunned**: The target is staggered until the end of his next turn, "+difficulty_die] * 5 + [
"**Stinger**: Increase the difficulty of next check by one, "+difficulty_die] * 5 + [
"**Bowled Over**: The target is knocked prone and suffers 1 strain, "+difficulty_die+difficulty_die] * 5 + [
"**Head Ringer**: The target increases the difficulty of all Intellect and Cunning checks by one until the end of the encounter, "+difficulty_die+difficulty_die] * 5 + [
"**Fearsome Wound**: The target increases the difficulty of all Presence and Willpower checks by one until the end of the encounter, "+difficulty_die+difficulty_die] * 5 + [
"**Agonizing Wound**: The target increases the difficulty of all Brawn and Agility checks by one until the end of the encounter, "+difficulty_die+difficulty_die] * 5 + [
"**Slightly Dazed**: The target is disoriented until the end of the encounter, "+difficulty_die+difficulty_die] * 5 + [
"**Scattered Senses**: The target removes all "+boost_die+" from skill checks until the end of the encounter, "+difficulty_die+difficulty_die] * 5 + [
"**Hamstrung**: The target loses his free maneuver until the end of the encounter, "+difficulty_die+difficulty_die] * 5 + [
"**Overpowered**: The target leaves himself open, and the attacker may immediately attempt another free attack agains him, using the exact same pool as the original, "+difficulty_die+difficulty_die] * 5 + [
"**Winded**: Until the end of the encounter, the target cannot voluntarily suffer strain to activate any abilities or gain additional maneuvers, "+difficulty_die+difficulty_die] * 5 + [
"**Compromised**: Incerase difficulty of all skill checks by one until the end of the encounter, "+difficulty_die+difficulty_die] * 5 + [
"**At the brink**: The target suffers 1 strain each time he performs an action, "+difficulty_die+difficulty_die+difficulty_die] * 5 + [
"**Crippled**: One of the target's limbs (selected by the GM) is crippled until healed or replaced. Increase difficulty of all checks that require use of that limb by one, "+difficulty_die+difficulty_die+difficulty_die] * 5 + [
"**Maimed**: One of the target's limbs (selected by the GM) is permanently lost. Unless the target has a cybernetic replacement, the target cannot perform actions that would require the use of that limb. All other actions gain "+setback_die+", "+difficulty_die+difficulty_die+difficulty_die] * 5 + [
"HI"] * 5 + [
"**Temporarily Lame**: Until this critical injury is healed, the target cannot perform more than one maneuver during his turn, "+difficulty_die+difficulty_die+difficulty_die] * 5 + [
"**Blinded**: The target can no longer see. Upgrade the difficulty of all checks twice. Upgrade the difficulty of perception checks three times, "+difficulty_die+difficulty_die+difficulty_die] * 5 + [
"**Knocked Senseless**: The target is staggered for the remainder of the encounter, "+difficulty_die+difficulty_die+difficulty_die] * 5 + [
"GI"] * 5 + [
"**Bleeding Out**: Every round, the target suffers 1 wound and 1 strain at the beginning of his turn. For every five wounds he suffers beyond his wound threshold, he suffers one additional critical injury. (If he suffers this one again, roll again), "+difficulty_die+difficulty_die+difficulty_die+difficulty_die] * 10 + [
"**The End is Nigh**: The target will die after the last initiative slot during the next round, "+difficulty_die+difficulty_die+difficulty_die+difficulty_die] * 10 + [
"**Dead**: U B Dead :("]
if roll >= len(injuries):
results = injuries[-1]
else:
results = injuries[roll]
if results == "HI":
characteristic = random.choice(["brawn"] * 3 + ["agility"] * 3 + ["intellect", "cunning", "presence"])
results = "**Horrific Injury**: Until this criticil injury is healed, treat the target's "+characteristic+" as if it's one lower, "+difficulty_die+difficulty_die+difficulty_die
if results == "GI":
characteristic = random.choice(["brawn"] * 3 + ["agility"] * 3 + ["intellect", "cunning", "presence"])
results = "**Gruesome Injury**: The target's "+characteristic+" is permanently one lower, "+difficulty_die+difficulty_die+difficulty_die+difficulty_die
send_message = "Roll: "+str(roll)+"\nInjury:\n"+results
message_list = send_message.split("\n")
await ctx.send(message_list[0])
if len(message_list) > 1:
for message_item in message_list[1:]:
await ctx.channel.send(message_item)
# Parses the command into something the other functions understand
async def parse_roll(self, ctx, cmd : str = ""):
user = f"#{ctx.author.id}"
cmd = re.sub(' +',' ',cmd.upper()) + " "
if cmd[0] == " ":
cmd = cmd[1:]
cmd = self.bot.star_wars.character.replaceSpaces(string.capwords(cmd))
commands = cmd.split(" ")
valid_command = False
if commands[0] == "":
roll_parameters = [1,0,3,0,0,0,0]
else:
roll_parameters = [0,0,0,0,0,0,0]
if string.capwords(commands[0]) == "Obligations":
send_message = self.obligation_roll()
elif string.capwords(commands[0]) in skill_data:
self.bot.log("Oh look! This guy has skills!")
if self.bot.star_wars.character.userHasChar(user):
self.bot.log("They have a character. That much we know")
skill_level = self.bot.star_wars.character.char_data(user,"Skills " + string.capwords(commands[0]))
if string.capwords(commands[0]) == "Lightsaber":
self.bot.log("The skill is lightsaber")
char_level = self.bot.star_wars.character.char_data(user,"Characteristics " + self.bot.star_wars.character.lightsaberChar(user))
else:
char_level = self.bot.star_wars.character.char_data(user,"Characteristics " + skill_data[string.capwords(commands[0])])
ability_dice = abs(char_level-skill_level)
proficiency_dice = min(skill_level,char_level)
commands = [str(ability_dice)] + [str(proficiency_dice)] + commands[1:]
self.bot.log("Converted skill to dice")
valid_command = True
else:
self.bot.log("Okay, no they don't i guess")
send_message = "You don't have a user. You can make one with /starwarscharacter"
elif string.capwords(commands[0]) in ["Ranged","Piloting"]:
self.bot.log("They fucked up writing the name of a ranged or piloting skill")
if string.capwords(commands[0]) == "Ranged":
send_message = "Did you mean \"Ranged - Heavy\" or \"Ranged - Light\""
else:
send_message = "Did you mean \"Piloting - Planetary\" or \"Piloting - Space\""
else:
valid_command = True
if valid_command:
self.bot.log("Converting commands to dice")
for i, command in enumerate(commands):
if command != "":
command = command.upper()
if command[0] == "A":
roll_parameters[0] = int(command.replace("A",""))
elif command[0] == "P":
roll_parameters[1] = int(command.replace("P",""))
elif command[0] == "D":
roll_parameters[2] = int(command.replace("D",""))
elif command[0] == "C":
roll_parameters[3] = int(command.replace("C",""))
elif command[0] == "B":
roll_parameters[4] = int(command.replace("B",""))
elif command[0] == "S":
roll_parameters[5] = int(command.replace("S",""))
elif command[0] == "F":
roll_parameters[6] = int(command.replace("F",""))
else:
roll_parameters[i] = int(command)
self.bot.log("Rolling "+str(roll_parameters))
roll_results, dice_results = self.roll(roll_parameters[0],roll_parameters[1],roll_parameters[2],roll_parameters[3],roll_parameters[4],roll_parameters[5],roll_parameters[6])
simplified = self.simplify(roll_results)
name = self.bot.star_wars.character.getChar_name(user)
self.bot.log("Returns results and simplified results")
if simplified == "":
send_message = name + " rolls: " + "\n" + self.dice_result_to_emoji(dice_results) + "\nEverything cancels out!"
else:
send_message = name + " rolls: " + "\n" + self.dice_result_to_emoji(dice_results) + "\n" + self.result_to_emoji(simplified)
message_list = send_message.split("\n")
await ctx.send(message_list[0])
if len(message_list) > 1:
for message_item in message_list[1:]:
if message_item == "":
self.bot.log("Tried to send empty message")
else:
await ctx.channel.send(message_item)

View File

@@ -0,0 +1,132 @@
"""
Contains the Gwendolyn class, a subclass of the discord command bot.
*Classes*
---------
Gwendolyn(discord.ext.commands.Bot)
"""
import os # Used for loading cogs in Gwendolyn.addCogs
import discord # Used for discord.Intents and discord.Status
import interactions # Used to initialized SlashCommands object
import git # Used to pull when stopping
from discord.ext import commands # Used to inherit from commands.bot
from pymongo import MongoClient # Used for database management
from gwendolyn_old.funcs import Money, StarWars, Games, Other, LookupFuncs
from gwendolyn_old.utils import (get_options, get_credentials, log_this,
DatabaseFuncs, EventHandler, ErrorHandler,
long_strings)
class Gwendolyn(commands.Bot):
"""
A multifunctional Discord bot.
*Methods*
---------
log(messages: Union[str, list], channel: str = "",
level: int = 20)
stop(ctx: interactions.SlashContext)
defer(ctx: interactions.SlashContext)
"""
# pylint: disable=too-many-instance-attributes
def __init__(self):
"""Initialize the bot."""
intents = discord.Intents.default()
intents.members = True
initiation_parameters = {
"command_prefix": " ",
"case_insensitive": True,
"intents": intents,
"status": discord.Status.dnd
}
super().__init__(**initiation_parameters)
self._add_clients_and_options()
self._add_util_classes()
self._add_function_containers()
self._add_cogs()
def _add_clients_and_options(self):
"""Add all the client, option and credentials objects."""
self.long_strings = long_strings()
self.options = get_options()
self.credentials = get_credentials()
mongo_user = self.credentials["mongo_db_user"]
mongo_password = self.credentials["mongo_db_password"]
mongo_url = f"mongodb+srv://{mongo_user}:{mongo_password}@gwendolyn"
mongo_url += ".qkwfy.mongodb.net/Gwendolyn?retryWrites=true&w=majority"
database_clint = MongoClient(mongo_url)
if self.options["testing"]:
self.log("Testing mode")
self.database = database_clint["Gwendolyn-Test"]
else:
self.database = database_clint["Gwendolyn"]
def _add_util_classes(self):
"""Add all the classes used as utility."""
self.database_funcs = DatabaseFuncs(self)
self.event_handler = EventHandler(self)
self.error_handler = ErrorHandler(self)
slash_parameters = {
"sync_commands": True,
"sync_on_cog_reload": True,
"override_type": True
}
self.slash = interactions.SlashCommand(self, **slash_parameters)
def _add_function_containers(self):
"""Add all the function containers used for commands."""
self.star_wars = StarWars(self)
self.other = Other(self)
self.lookup_funcs = LookupFuncs(self)
self.games = Games(self)
self.money = Money(self)
def _add_cogs(self):
"""Load cogs."""
for filename in os.listdir("./gwendolyn/cogs"):
if filename.endswith(".py"):
self.load_extension(f"gwendolyn.cogs.{filename[:-3]}")
def log(self, messages, channel: str = "", level: int = 20): # pylint:disable=no-self-use
"""Log a message. Described in utils/util_functions.py."""
log_this(messages, channel, level)
async def stop(self, ctx: interactions.SlashContext):
"""
Stop the bot, and stop running games.
Only stops the bot if the user in ctx.author is one of the
admins given in options.txt.
*parameters*
------------
ctx: interactions.SlashContext
The context of the "/stop" slash command.
"""
if f"#{ctx.author.id}" in self.options["admins"]:
await ctx.send("Pulling git repo and restarting...")
await self.change_presence(status=discord.Status.offline)
self.database_funcs.wipe_games()
if not self.options["testing"]:
git_client = git.cmd.Git("")
git_client.pull()
self.log("Logging out", level=25)
await self.close()
else:
log_message = f"{ctx.author.display_name} tried to stop me!"
self.log(log_message, str(ctx.channel_id))
await ctx.send(f"I don't think I will, {ctx.author.display_name}")
async def defer(self, ctx: interactions.SlashContext):
"""Send a "Gwendolyn is thinking" message to the user."""
try:
await ctx.defer()
except interactions.error.AlreadyResponded:
self.log("defer failed")

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Some files were not shown because too many files have changed in this diff Show More