reworking a bunch of stuff

This commit is contained in:
Nikolaj
2024-10-28 13:05:06 +01:00
parent f21cbba726
commit bc59bf9b05
142 changed files with 1385 additions and 845 deletions

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)