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.
- easterobot/alembic/env.py +91 -0
- easterobot/alembic/script.py.mako +28 -0
- easterobot/alembic/versions/2f0d4305e320_init_database.py +67 -0
- easterobot/alembic/versions/940c3b9c702d_add_lock_on_eggs.py +38 -0
- easterobot/bot.py +93 -462
- easterobot/cli.py +56 -17
- easterobot/commands/__init__.py +8 -0
- easterobot/commands/base.py +3 -7
- easterobot/commands/basket.py +10 -12
- easterobot/commands/edit.py +4 -4
- easterobot/commands/game.py +187 -0
- easterobot/commands/help.py +1 -1
- easterobot/commands/reset.py +1 -1
- easterobot/commands/search.py +3 -3
- easterobot/commands/top.py +7 -18
- easterobot/config.py +67 -3
- easterobot/games/__init__.py +14 -0
- easterobot/games/connect.py +206 -0
- easterobot/games/game.py +262 -0
- easterobot/games/rock_paper_scissor.py +206 -0
- easterobot/games/tic_tac_toe.py +168 -0
- easterobot/hunts/__init__.py +14 -0
- easterobot/hunts/hunt.py +428 -0
- easterobot/hunts/rank.py +82 -0
- easterobot/models.py +2 -1
- easterobot/resources/alembic.ini +87 -0
- easterobot/resources/config.example.yml +10 -2
- easterobot/resources/credits.txt +5 -1
- easterobot/resources/emotes/icons/arrow.png +0 -0
- easterobot/resources/emotes/icons/end.png +0 -0
- easterobot/resources/emotes/icons/versus.png +0 -0
- easterobot/resources/emotes/icons/wait.png +0 -0
- {easterobot-1.0.0.dist-info → easterobot-1.1.0.dist-info}/METADATA +11 -5
- easterobot-1.1.0.dist-info/RECORD +66 -0
- easterobot-1.0.0.dist-info/RECORD +0 -48
- /easterobot/resources/{eggs → emotes/eggs}/egg_01.png +0 -0
- /easterobot/resources/{eggs → emotes/eggs}/egg_02.png +0 -0
- /easterobot/resources/{eggs → emotes/eggs}/egg_03.png +0 -0
- /easterobot/resources/{eggs → emotes/eggs}/egg_04.png +0 -0
- /easterobot/resources/{eggs → emotes/eggs}/egg_05.png +0 -0
- /easterobot/resources/{eggs → emotes/eggs}/egg_06.png +0 -0
- /easterobot/resources/{eggs → emotes/eggs}/egg_07.png +0 -0
- /easterobot/resources/{eggs → emotes/eggs}/egg_08.png +0 -0
- /easterobot/resources/{eggs → emotes/eggs}/egg_09.png +0 -0
- /easterobot/resources/{eggs → emotes/eggs}/egg_10.png +0 -0
- /easterobot/resources/{eggs → emotes/eggs}/egg_11.png +0 -0
- /easterobot/resources/{eggs → emotes/eggs}/egg_12.png +0 -0
- /easterobot/resources/{eggs → emotes/eggs}/egg_13.png +0 -0
- /easterobot/resources/{eggs → emotes/eggs}/egg_14.png +0 -0
- /easterobot/resources/{eggs → emotes/eggs}/egg_15.png +0 -0
- /easterobot/resources/{eggs → emotes/eggs}/egg_16.png +0 -0
- /easterobot/resources/{eggs → emotes/eggs}/egg_17.png +0 -0
- /easterobot/resources/{eggs → emotes/eggs}/egg_18.png +0 -0
- /easterobot/resources/{eggs → emotes/eggs}/egg_19.png +0 -0
- /easterobot/resources/{eggs → emotes/eggs}/egg_20.png +0 -0
- {easterobot-1.0.0.dist-info → easterobot-1.1.0.dist-info}/WHEEL +0 -0
- {easterobot-1.0.0.dist-info → easterobot-1.1.0.dist-info}/entry_points.txt +0 -0
- {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)
|