From f9a3c07ea323d6e6f3a1920519a804b7f67430dd Mon Sep 17 00:00:00 2001 From: B1ue-Dev <60958064+B1ue-Dev@users.noreply.github.com> Date: Thu, 6 Jul 2023 14:37:16 +0700 Subject: [PATCH 1/6] fix: url being None --- exts/utils/snipe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exts/utils/snipe.py b/exts/utils/snipe.py index 6feafce..cf3a553 100644 --- a/exts/utils/snipe.py +++ b/exts/utils/snipe.py @@ -37,7 +37,7 @@ async def on_message_delete( ) _snipe_message_author_id[_channel_id] = str(message.author.id) _snipe_message_author_avatar_url[_channel_id] = str( - message.author.avatar.url + message.author.avatar.url if message.author.avatar else None ) _snipe_message_content[_channel_id] = str(message.content) _snipe_message_content_id[_channel_id] = str(message.id) From c7fcee779c5cbbb60a4d4662ce0fc1fe6ca80f0d Mon Sep 17 00:00:00 2001 From: B1ue-Dev <60958064+B1ue-Dev@users.noreply.github.com> Date: Thu, 6 Jul 2023 14:38:12 +0700 Subject: [PATCH 2/6] fix: Check for `message` and `member` --- exts/server/logs.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/exts/server/logs.py b/exts/server/logs.py index b929049..1c87e7b 100644 --- a/exts/server/logs.py +++ b/exts/server/logs.py @@ -48,7 +48,7 @@ async def on_message_delete( embed = interactions.Embed( title="Deleted message content", description=message.content[-4096:] - if message.content + if isinstance(message, interactions.Message) and message.content else "No content found. (Maybe it is not cached)", color=0xE03C3C, author=author, @@ -56,7 +56,7 @@ async def on_message_delete( timestamp=datetime.datetime.utcnow(), fields=fields, ) - if message.attachments: + if isinstance(message, interactions.Message) and message.attachments: embed.add_field( name="Attachment", value="\n".join( @@ -115,7 +115,10 @@ async def on_message_update( ) embed.add_field( name="Message after edit", - value=after.content[-1024:] if after.content != [] else "N/A", + value=after.content[-1024:] + if after.content + and not isinstance(after, interactions.MISSING) != [] + else "N/A", ) for channel in after.guild.channels: @@ -186,10 +189,13 @@ async def on_guild_member_remove( color=random.randint(0, 0xFFFFFF), timestamp=datetime.datetime.utcnow(), footer=interactions.EmbedFooter(text=f"ID: {member.id}"), - thumbnail=interactions.EmbedAttachment( - url=member.user.avatar.url if member.user.avatar else None - ), ) + if isinstance(member, interactions.User): + embed.set_thumbnail(member.avatar.url if member.avatar else None) + elif isinstance(member, interactions.Member): + embed.set_thumbnail( + member.user.avatar.url if member.avatar else None + ) for channel in member.guild.channels: if channel.name == "welcome-goodbye" and int(channel.type) == 0: From cff9d97f9fa9c9f7c65acc772f94692914726be4 Mon Sep 17 00:00:00 2001 From: B1ue-Dev <60958064+B1ue-Dev@users.noreply.github.com> Date: Thu, 6 Jul 2023 15:00:16 +0700 Subject: [PATCH 3/6] feat: TicTacToe command --- exts/fun/tictactoe.py | 274 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 274 insertions(+) create mode 100644 exts/fun/tictactoe.py diff --git a/exts/fun/tictactoe.py b/exts/fun/tictactoe.py new file mode 100644 index 0000000..232453d --- /dev/null +++ b/exts/fun/tictactoe.py @@ -0,0 +1,274 @@ +import logging +import asyncio +import enum +import random +import copy +import math +import interactions +from interactions.ext.hybrid_commands import ( + hybrid_slash_subcommand, + HybridContext, +) + + +class GameState(enum.IntEnum): + empty = 0 + player = -1 + ai = +1 + + +def render_board(board: list, disable=False) -> list: + """ + Converts the test_board into a visual representation using discord components + :param board: The game test_board + :param disable: Disable the buttons on the test_board + :return: List[action-rows] + """ + + buttons = [] + for i in range(3): + for x in range(3): + if board[i][x] == GameState.empty: + style = interactions.ButtonStyle.GRAY + elif board[i][x] == GameState.player: + style = interactions.ButtonStyle.PRIMARY + else: + style = interactions.ButtonStyle.RED + buttons.append( + interactions.Button( + style=style, + label="‎", + custom_id=f"tic_tac_toe_button||{i},{x}", + disabled=disable, + ) + ) + return interactions.spread_to_rows(*buttons, max_in_row=3) + + +def board_state(components: list) -> list[list]: + """ + Extrapolate the current state of the game based on the components of a message + :param components: The components object from a message + :return: The test_board state + :rtype: list[list] + """ + + board = copy.deepcopy(BoardTemplate) + for i in range(3): + for x in range(3): + button = components[i].components[x] + if button.style == 2: + board[i][x] = GameState.empty + elif button.style == 1: + board[i][x] = GameState.player + elif button.style == 4: + board[i][x] = GameState.ai + return board + + +def win_state(board: list, player: GameState) -> bool: + """ + Determines if the specified player has won + :param board: The game test_board + :param player: The player to check for + :return: bool, have they won + """ + win_states = [ + [board[0][0], board[0][1], board[0][2]], + [board[1][0], board[1][1], board[1][2]], + [board[2][0], board[2][1], board[2][2]], + [board[0][0], board[1][0], board[2][0]], + [board[0][1], board[1][1], board[2][1]], + [board[0][2], board[1][2], board[2][2]], + [board[0][0], board[1][1], board[2][2]], + [board[2][0], board[1][1], board[0][2]], + ] + if [player, player, player] in win_states: + return True + return False + + +def get_possible_positions(board: list) -> list[list[int]]: + """ + Determines all the possible positions in the current game state + :param board: The game test_board + :return: A list of possible positions + """ + + possible_positions = [] + for i in range(3): + for x in range(3): + if board[i][x] == GameState.empty: + possible_positions.append([i, x]) + return possible_positions + + +def evaluate(board): + if win_state(board, GameState.ai): + score = +1 + elif win_state(board, GameState.player): + score = -1 + else: + score = 0 + return score + + +def min_max(test_board: list, depth: int, player: GameState): + if player == GameState.ai: + best = [-1, -1, -math.inf] + else: + best = [-1, -1, +math.inf] + + if ( + depth == 0 + or win_state(test_board, GameState.player) + or win_state(test_board, GameState.ai) + ): + score = evaluate(test_board) + return [-1, -1, score] + + for cell in get_possible_positions(test_board): + x, y = cell[0], cell[1] + test_board[x][y] = player + score = min_max(test_board, depth - 1, -player) + test_board[x][y] = GameState.empty + score[0], score[1] = x, y + + if player == GameState.ai: + if score[2] > best[2]: + best = score + else: + if score[2] < best[2]: + best = score + return best + + +BoardTemplate = [[0, 0, 0], [0, 0, 0], [0, 0, 0]] + + +class TicTacToe(interactions.Extension): + def __init__(self, client: interactions.Client): + self.client: interactions.Client = client + + @hybrid_slash_subcommand( + base="tic_tac_toe", + base_description="TicTacToe command.", + aliases=["ttt", "tic", "tac", "toe"], + name="easy", + description="Play TicTacToe in easy mode.", + ) + async def ttt_easy(self, ctx: HybridContext) -> None: + """Play TicTacToe in easy mode.""" + + await ctx.send( + content=f"{ctx.author.mention}'s tic tac toe game (easy mode)", + components=render_board(copy.deepcopy(BoardTemplate)), + ) + + @hybrid_slash_subcommand( + base="tic_tac_toe", + base_description="TicTacToe command.", + name="hard", + description="Play TicTacToe in hard mode.", + ) + async def ttt_hard(self, ctx: HybridContext) -> None: + """Play TicTacToe in hard mode.""" + + await ctx.send( + content=f"{ctx.author.mention}'s tic tac toe game (hard mode)", + components=render_board(copy.deepcopy(BoardTemplate)), + ) + + @interactions.component_callback( + interactions.get_components_ids( + render_board(board=copy.deepcopy(BoardTemplate)) + ) + ) + async def process_turn(self, ctx: interactions.ComponentContext) -> None: + await ctx.defer(edit_origin=True) + try: + async for user in ctx.message.mention_users: + if ctx.author.id != user.id: + return + except Exception as ex: + print(ex) + breakpoint() + return + + button_pos = (ctx.custom_id.split("||")[-1]).split(",") + button_pos = [int(button_pos[0]), int(button_pos[1])] + components = ctx.message.components + + board = board_state(components) + + # Easy mode + # Articuno will get all possible moves, and use the random + # module to get a position. + if ctx.message.content.find("easy mode") != -1: + print("Easy") + if board[button_pos[0]][button_pos[1]] == GameState.empty: + board[button_pos[0]][button_pos[1]] = GameState.player + if not win_state(board, GameState.player): + if len(get_possible_positions(board)) != 0: + ai_pos = random.choice(get_possible_positions(board)) + x, y = ai_pos[0], ai_pos[1] + board[x][y] = GameState.ai + else: + return + + # Hard mode + # Articuno will use minimax to make sure that the game will + # never be a win for the player. + elif ctx.message.content.find("hard mode") != -1: + print("Hard") + if board[button_pos[0]][button_pos[1]] == GameState.empty: + board[button_pos[0]][button_pos[1]] = GameState.player + if not win_state(board, GameState.player): + possible_positions = get_possible_positions(board) + # ai pos + if len(possible_positions) != 0: + depth = len(possible_positions) + + move = await asyncio.to_thread( + min_max, + copy.deepcopy(board), + min(random.choice([4, 6]), depth), + GameState.ai, + ) + x, y = move[0], move[1] + board[x][y] = GameState.ai + else: + return + + if win_state(board, GameState.player): + winner = ctx.author.mention + elif win_state(board, GameState.ai): + winner = self.bot.user.mention + elif len(get_possible_positions(board)) == 0: + winner = "Nobody" + else: + winner = None + + content = f"{ctx.author.mention}'s tic tac toe game " + ( + "(easy mode)" + if ctx.message.content.find("easy mode") != -1 + else "(hard mode)" + ) + + await ctx.edit_origin( + content=content + if not winner + else f"{winner} has won! " + + ( + "(easy mode)" + if ctx.message.content.find("easy mode") != -1 + else "(hard mode)" + ), + components=render_board(board, disable=winner is not None), + ) + + +def setup(client) -> None: + """Setup the extension.""" + TicTacToe(client) + logging.info("Loaded TicTacToe extension.") From 73a21a6fc0606ae744c9d2f3c1e304a794f1034f Mon Sep 17 00:00:00 2001 From: B1ue-Dev <60958064+B1ue-Dev@users.noreply.github.com> Date: Thu, 6 Jul 2023 15:00:41 +0700 Subject: [PATCH 4/6] fix: MISSING is not an actual object --- exts/server/logs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exts/server/logs.py b/exts/server/logs.py index 1c87e7b..93a0e9f 100644 --- a/exts/server/logs.py +++ b/exts/server/logs.py @@ -117,7 +117,7 @@ async def on_message_update( name="Message after edit", value=after.content[-1024:] if after.content - and not isinstance(after, interactions.MISSING) != [] + and not isinstance(after, type(interactions.MISSING)) else "N/A", ) From 605750b85855925c1bf2cc1e5565e21202890181 Mon Sep 17 00:00:00 2001 From: B1ue-Dev <60958064+B1ue-Dev@users.noreply.github.com> Date: Thu, 6 Jul 2023 15:01:10 +0700 Subject: [PATCH 5/6] chore: Version bump to v5.1.4 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index df084fc..040a90f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "Articuno" -version = "v5.1.3" +version = "v5.1.4" description = "A fun Discord Bot, written with interactions.py." license = "GPL-3.0-only" authors = ["B1ue-Dev "] From 97bd08534579c068276ec87865aab42f86386311 Mon Sep 17 00:00:00 2001 From: B1ue-Dev <60958064+B1ue-Dev@users.noreply.github.com> Date: Thu, 6 Jul 2023 15:03:23 +0700 Subject: [PATCH 6/6] chore: tic_tac_toe -> tictactoe name change --- exts/fun/tictactoe.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/exts/fun/tictactoe.py b/exts/fun/tictactoe.py index 232453d..5d6ef62 100644 --- a/exts/fun/tictactoe.py +++ b/exts/fun/tictactoe.py @@ -151,7 +151,7 @@ def __init__(self, client: interactions.Client): self.client: interactions.Client = client @hybrid_slash_subcommand( - base="tic_tac_toe", + base="tictactoe", base_description="TicTacToe command.", aliases=["ttt", "tic", "tac", "toe"], name="easy", @@ -166,7 +166,7 @@ async def ttt_easy(self, ctx: HybridContext) -> None: ) @hybrid_slash_subcommand( - base="tic_tac_toe", + base="tictactoe", base_description="TicTacToe command.", name="hard", description="Play TicTacToe in hard mode.",