easterobot 1.0.0__py3-none-any.whl → 1.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. easterobot/alembic/env.py +91 -0
  2. easterobot/alembic/script.py.mako +28 -0
  3. easterobot/alembic/versions/2f0d4305e320_init_database.py +67 -0
  4. easterobot/alembic/versions/940c3b9c702d_add_lock_on_eggs.py +38 -0
  5. easterobot/bot.py +93 -462
  6. easterobot/cli.py +56 -17
  7. easterobot/commands/__init__.py +8 -0
  8. easterobot/commands/base.py +3 -7
  9. easterobot/commands/basket.py +10 -12
  10. easterobot/commands/edit.py +4 -4
  11. easterobot/commands/game.py +187 -0
  12. easterobot/commands/help.py +1 -1
  13. easterobot/commands/reset.py +1 -1
  14. easterobot/commands/search.py +3 -3
  15. easterobot/commands/top.py +7 -18
  16. easterobot/config.py +67 -3
  17. easterobot/games/__init__.py +14 -0
  18. easterobot/games/connect.py +206 -0
  19. easterobot/games/game.py +262 -0
  20. easterobot/games/rock_paper_scissor.py +206 -0
  21. easterobot/games/tic_tac_toe.py +168 -0
  22. easterobot/hunts/__init__.py +14 -0
  23. easterobot/hunts/hunt.py +428 -0
  24. easterobot/hunts/rank.py +82 -0
  25. easterobot/models.py +2 -1
  26. easterobot/resources/alembic.ini +87 -0
  27. easterobot/resources/config.example.yml +10 -2
  28. easterobot/resources/credits.txt +5 -1
  29. easterobot/resources/emotes/icons/arrow.png +0 -0
  30. easterobot/resources/emotes/icons/end.png +0 -0
  31. easterobot/resources/emotes/icons/versus.png +0 -0
  32. easterobot/resources/emotes/icons/wait.png +0 -0
  33. {easterobot-1.0.0.dist-info → easterobot-1.1.0.dist-info}/METADATA +11 -5
  34. easterobot-1.1.0.dist-info/RECORD +66 -0
  35. easterobot-1.0.0.dist-info/RECORD +0 -48
  36. /easterobot/resources/{eggs → emotes/eggs}/egg_01.png +0 -0
  37. /easterobot/resources/{eggs → emotes/eggs}/egg_02.png +0 -0
  38. /easterobot/resources/{eggs → emotes/eggs}/egg_03.png +0 -0
  39. /easterobot/resources/{eggs → emotes/eggs}/egg_04.png +0 -0
  40. /easterobot/resources/{eggs → emotes/eggs}/egg_05.png +0 -0
  41. /easterobot/resources/{eggs → emotes/eggs}/egg_06.png +0 -0
  42. /easterobot/resources/{eggs → emotes/eggs}/egg_07.png +0 -0
  43. /easterobot/resources/{eggs → emotes/eggs}/egg_08.png +0 -0
  44. /easterobot/resources/{eggs → emotes/eggs}/egg_09.png +0 -0
  45. /easterobot/resources/{eggs → emotes/eggs}/egg_10.png +0 -0
  46. /easterobot/resources/{eggs → emotes/eggs}/egg_11.png +0 -0
  47. /easterobot/resources/{eggs → emotes/eggs}/egg_12.png +0 -0
  48. /easterobot/resources/{eggs → emotes/eggs}/egg_13.png +0 -0
  49. /easterobot/resources/{eggs → emotes/eggs}/egg_14.png +0 -0
  50. /easterobot/resources/{eggs → emotes/eggs}/egg_15.png +0 -0
  51. /easterobot/resources/{eggs → emotes/eggs}/egg_16.png +0 -0
  52. /easterobot/resources/{eggs → emotes/eggs}/egg_17.png +0 -0
  53. /easterobot/resources/{eggs → emotes/eggs}/egg_18.png +0 -0
  54. /easterobot/resources/{eggs → emotes/eggs}/egg_19.png +0 -0
  55. /easterobot/resources/{eggs → emotes/eggs}/egg_20.png +0 -0
  56. {easterobot-1.0.0.dist-info → easterobot-1.1.0.dist-info}/WHEEL +0 -0
  57. {easterobot-1.0.0.dist-info → easterobot-1.1.0.dist-info}/entry_points.txt +0 -0
  58. {easterobot-1.0.0.dist-info → easterobot-1.1.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,206 @@
1
+ """TicTacToe."""
2
+
3
+ from functools import partial
4
+ from typing import Optional
5
+
6
+ import discord
7
+ from typing_extensions import override
8
+
9
+ from easterobot.commands.base import Interaction
10
+ from easterobot.games.game import Button, Game
11
+
12
+ ROCK = b"\xf0\x9f\xaa\xa8".decode("utf-8")
13
+ PAPER = b"\xf0\x9f\x93\x84".decode("utf-8")
14
+ SCISSORS = b"\xe2\x9c\x82\xef\xb8\x8f".decode("utf-8")
15
+ EMOJIS = [PAPER, ROCK, SCISSORS]
16
+
17
+
18
+ class RockPaperScissor(Game):
19
+ view: discord.ui.View
20
+
21
+ def __init__(
22
+ self,
23
+ player1: discord.Member,
24
+ player2: discord.Member,
25
+ message: discord.Message,
26
+ win_count: int = 3,
27
+ max_turn: int = 10,
28
+ ) -> None:
29
+ """Initialize grid."""
30
+ self.timeout = False
31
+ self.max_turn = max_turn
32
+ self.win_count = win_count
33
+ self.player1 = player1
34
+ self.player2 = player2
35
+ self.play1: Optional[str] = None
36
+ self.play2: Optional[str] = None
37
+ self.history: list[tuple[str, str]] = []
38
+ super().__init__(message)
39
+
40
+ @override
41
+ async def on_start(self) -> None:
42
+ """Run."""
43
+ embed = discord.Embed(color=0xF2BC32)
44
+ embed.set_author(
45
+ name="Partie en cours", icon_url=self.bot.app_emojis["wait"].url
46
+ )
47
+ self.view = discord.ui.View()
48
+ rock_btn: Button = discord.ui.Button(
49
+ style=discord.ButtonStyle.gray,
50
+ emoji=ROCK,
51
+ )
52
+ paper_btn: Button = discord.ui.Button(
53
+ style=discord.ButtonStyle.gray,
54
+ emoji=PAPER,
55
+ )
56
+ scissor_btn: Button = discord.ui.Button(
57
+ style=discord.ButtonStyle.gray,
58
+ emoji=SCISSORS,
59
+ )
60
+
61
+ async def action_callback(
62
+ interaction: Interaction,
63
+ label: str,
64
+ ) -> None:
65
+ update = False
66
+
67
+ # Player 1 click on button
68
+ if interaction.user == self.player1 and self.play1 is None:
69
+ self.play1 = label
70
+ update = True
71
+
72
+ # Player 2 click on button
73
+ elif interaction.user == self.player2 and self.play2 is None:
74
+ self.play2 = label
75
+ update = True
76
+
77
+ # One button has been pressed
78
+ if update:
79
+ await interaction.response.defer()
80
+ await self.update()
81
+ else:
82
+ await interaction.response.send_message(
83
+ "Vous ne pouvez pas intéragir dans cette partie !",
84
+ ephemeral=True,
85
+ delete_after=5,
86
+ )
87
+
88
+ rock_btn.callback = partial(action_callback, label=ROCK) # type: ignore[method-assign]
89
+ paper_btn.callback = partial(action_callback, label=PAPER) # type: ignore[method-assign]
90
+ scissor_btn.callback = partial(action_callback, label=SCISSORS) # type: ignore[method-assign]
91
+ self.view.add_item(rock_btn)
92
+ self.view.add_item(paper_btn)
93
+ self.view.add_item(scissor_btn)
94
+ await self.update()
95
+
96
+ async def update(self) -> None: # noqa: PLR0912, PLR0915
97
+ """Update the current display."""
98
+ embed = discord.Embed(color=0xF2BC32)
99
+ # Both player have played
100
+ header = "Partie en cours"
101
+ if self.play1 is None:
102
+ icon_url = self.player1.display_avatar.url
103
+ info = f"En attente de {self.player1.mention} ..."
104
+ elif self.play2 is None:
105
+ icon_url = self.player2.display_avatar.url
106
+ info = f"En attente de {self.player2.mention} ..."
107
+ else:
108
+ # Play and fight
109
+ self.history.append((self.play1, self.play2))
110
+ self.play1 = None
111
+ self.play2 = None
112
+ icon_url = self.bot.app_emojis["wait"].url
113
+ info = (
114
+ f"En attente de {self.player1.mention} "
115
+ f"et {self.player2.mention} ..."
116
+ )
117
+ pt1 = 0
118
+ pt2 = 0
119
+ morsels = []
120
+ for play1, play2 in self.history:
121
+ i1 = EMOJIS.index(play1)
122
+ i2 = EMOJIS.index(play2)
123
+ if i1 == (i2 + 1) % 3:
124
+ pt1 += 1
125
+ text = self.player1.mention
126
+ elif i1 == (i2 - 1) % 3:
127
+ pt2 += 1
128
+ text = self.player2.mention
129
+ else:
130
+ text = "**égalité**"
131
+ morsels.append(
132
+ f"### {play1} {self.bot.app_emojis['versus']} {play2} "
133
+ f"{self.bot.app_emojis['arrow']} {text}"
134
+ )
135
+ embed.description = "\n".join(morsels)
136
+ if (
137
+ len(self.history) >= self.max_turn
138
+ or pt1 >= self.win_count
139
+ or pt2 >= self.win_count
140
+ or self.timeout
141
+ ):
142
+ header = "Partie terminée"
143
+ embed.description += "\n\n"
144
+ if self.timeout:
145
+ final_winner = self.winner
146
+ elif pt1 < pt2:
147
+ final_winner = self.player2
148
+ elif pt2 < pt1:
149
+ final_winner = self.player1
150
+ else:
151
+ final_winner = None
152
+ if final_winner:
153
+ forfait = "par forfait " if self.timeout else ""
154
+ embed.description += (
155
+ f"## Gagnant {forfait}{final_winner.mention} 🎉"
156
+ )
157
+ icon_url = final_winner.display_avatar.url
158
+ else:
159
+ embed.description += (
160
+ f"## Égalité entre {self.player1.mention} "
161
+ f"et {self.player2.mention} 🤝"
162
+ )
163
+ icon_url = self.bot.app_emojis["end"].url
164
+ self.view.stop()
165
+ self.view.clear_items()
166
+ if not self.timeout:
167
+ await self.set_winner(final_winner)
168
+ else:
169
+ dt = self.start_timer(63)
170
+ embed.description += f"\n\n{info}\n\nFin du tour {dt}"
171
+ embed.set_author(name=header, icon_url=icon_url)
172
+ await self.message.edit(embed=embed, view=self.view, content="")
173
+
174
+ def compute_winner(
175
+ self, play1: str, play2: str
176
+ ) -> Optional[discord.Member]:
177
+ """Get the winner."""
178
+ i1 = EMOJIS.index(play1)
179
+ i2 = EMOJIS.index(play2)
180
+ if i1 == (i2 + 1) % 3:
181
+ return self.player1
182
+ if i1 == (i2 - 1) % 3:
183
+ return self.player2
184
+ return None # Draw
185
+
186
+ def color(
187
+ self, member: Optional[discord.Member]
188
+ ) -> Optional[discord.Colour]:
189
+ """Color of the embed."""
190
+ if member is None:
191
+ return discord.Colour.from_str("#d4d5d6")
192
+ if member == self.player1:
193
+ return discord.Colour.from_str("#ca2a3e")
194
+ if member == self.player2:
195
+ return discord.Colour.from_str("#5865F2")
196
+ error_message = f"Invalid member: {member!r}"
197
+ raise ValueError(error_message)
198
+
199
+ @override
200
+ async def on_timeout(self) -> None:
201
+ self.timeout = True
202
+ if self.play1 is None:
203
+ await self.set_winner(self.player2)
204
+ if self.play2 is None:
205
+ await self.set_winner(self.player1)
206
+ await self.update()
@@ -0,0 +1,168 @@
1
+ """TicTacToe."""
2
+
3
+ import datetime
4
+ from typing import Optional
5
+
6
+ import discord
7
+ from discord.utils import format_dt
8
+ from typing_extensions import override
9
+
10
+ from easterobot.games.game import Game, Player
11
+
12
+ EMOJIS_MAPPER = {
13
+ "1️⃣": 0,
14
+ "2️⃣": 1,
15
+ "3️⃣": 2,
16
+ "4️⃣": 3,
17
+ "5️⃣": 4,
18
+ "6️⃣": 5,
19
+ "7️⃣": 6,
20
+ "8️⃣": 7,
21
+ "9️⃣": 8,
22
+ }
23
+ EMOJIS = tuple(EMOJIS_MAPPER)
24
+
25
+
26
+ class TicTacToe(Game):
27
+ def __init__(
28
+ self,
29
+ player1: discord.Member,
30
+ player2: discord.Member,
31
+ message: discord.Message,
32
+ ) -> None:
33
+ """Initialize grid."""
34
+ self.grid: list[Optional[Player]] = [None] * 9
35
+ self.timeout = False
36
+ self.player1 = Player(player1, 1)
37
+ self.player2 = Player(player2, 2)
38
+ self.turn = 0
39
+ super().__init__(message)
40
+
41
+ @override
42
+ async def on_start(self) -> None:
43
+ """Run."""
44
+ await self.update()
45
+ for emoji in EMOJIS:
46
+ await self.message.add_reaction(emoji)
47
+ self.start_timer(60)
48
+
49
+ async def update(self) -> None:
50
+ """Update the message."""
51
+ label = ""
52
+ footer = ""
53
+ if not self.terminate:
54
+ label = "⭕" if self.turn % 2 else "❌"
55
+ label += f" Joueur actuel : {self.current.member.mention}\n\n"
56
+ user: Optional[Player] = self.current
57
+ elif self.winner:
58
+ forfait = "par forfait " if self.timeout else ""
59
+ footer = f"\n## Gagnant {forfait}{self.winner.mention} 🎉"
60
+ user = self.current
61
+ else:
62
+ footer = (
63
+ "\n## Égalité entre "
64
+ f"{self.player1.member.mention} "
65
+ f"et {self.player2.member.mention} 🤝"
66
+ )
67
+ user = None
68
+
69
+ content = label
70
+ lines = []
71
+ for row in range(3):
72
+ pieces = []
73
+ for col in range(3):
74
+ index = row * 3 + col
75
+ player = self.grid[index]
76
+ if player == self.player1:
77
+ piece = "❌"
78
+ elif player == self.player2:
79
+ piece = "⭕"
80
+ else:
81
+ piece = EMOJIS[index]
82
+ pieces.append(piece)
83
+ lines.append("│".join(pieces))
84
+ content += "\n".join(lines)
85
+ content += footer
86
+
87
+ if not self.terminate:
88
+ now = datetime.datetime.now() + datetime.timedelta(seconds=62) # noqa: DTZ005
89
+ content += f"\n\nFin du tour {format_dt(now, style='R')}"
90
+
91
+ embed = discord.Embed(description=content, color=self.color(user))
92
+ embed.set_author(
93
+ name="Partie terminée" if self.terminate else "Partie en cours",
94
+ icon_url=(
95
+ user.member.display_avatar.url
96
+ if user
97
+ else self.bot.app_emojis["end"].url
98
+ ),
99
+ )
100
+ self.message = await self.message.edit(
101
+ embed=embed,
102
+ content="",
103
+ view=None,
104
+ )
105
+
106
+ @override
107
+ async def on_reaction(
108
+ self, member_id: int, reaction: discord.PartialEmoji
109
+ ) -> None:
110
+ if reaction.name not in EMOJIS or member_id != self.current.member.id:
111
+ return
112
+ index = EMOJIS_MAPPER[reaction.name]
113
+ await self.place(index, self.current)
114
+
115
+ @property
116
+ def current(self) -> Player:
117
+ """Get current member."""
118
+ return [self.player1, self.player2][self.turn % 2]
119
+
120
+ def color(self, player: Optional[Player]) -> Optional[discord.Colour]:
121
+ """Color of the embed."""
122
+ if player is None:
123
+ return discord.Colour.from_str("#d4d5d6")
124
+ if player == self.player1:
125
+ return discord.Colour.from_str("#F17720")
126
+ if player == self.player2:
127
+ return discord.Colour.from_str("#0474BA")
128
+ error_message = f"Invalid player: {player!r}"
129
+ raise ValueError(error_message)
130
+
131
+ async def place(self, index: int, player: Player) -> None:
132
+ """Place a piece."""
133
+ if self.terminate or self.grid[index] is not None:
134
+ return
135
+ async with self.lock:
136
+ await self.stop_timer()
137
+ self.grid[index] = player
138
+
139
+ if self._is_winner(player):
140
+ await self.set_winner(player.member)
141
+ elif all(cell is not None for cell in self.grid):
142
+ await self.set_winner(None)
143
+ else:
144
+ self.turn += 1
145
+ self.start_timer(60)
146
+ await self.update()
147
+
148
+ @override
149
+ async def on_timeout(self) -> None:
150
+ self.turn += 1
151
+ self.timeout = True
152
+ await self.set_winner(self.current.member)
153
+ await self.update()
154
+
155
+ def _is_winner(self, player: Player) -> bool:
156
+ wins = [
157
+ [0, 1, 2],
158
+ [3, 4, 5],
159
+ [6, 7, 8], # rows
160
+ [0, 3, 6],
161
+ [1, 4, 7],
162
+ [2, 5, 8], # cols
163
+ [0, 4, 8],
164
+ [2, 4, 6], # diagonals
165
+ ]
166
+ return any(
167
+ all(self.grid[i] == player for i in combo) for combo in wins
168
+ )
@@ -0,0 +1,14 @@
1
+ """Enable hunt."""
2
+
3
+ from easterobot.bot import Easterobot
4
+ from easterobot.hunts.hunt import HuntCog
5
+
6
+ __all__ = [
7
+ "HuntCog",
8
+ ]
9
+
10
+
11
+ async def setup(bot: Easterobot) -> None:
12
+ hunt_cog = HuntCog(bot)
13
+ bot.hunt = hunt_cog
14
+ await bot.add_cog(hunt_cog)