easterobot 1.1.1__py3-none-any.whl → 1.3.1__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/bot.py +15 -6
- easterobot/commands/__init__.py +3 -0
- easterobot/commands/base.py +4 -1
- easterobot/commands/disable.py +2 -1
- easterobot/commands/edit.py +2 -1
- easterobot/commands/enable.py +2 -1
- easterobot/commands/game.py +26 -22
- easterobot/commands/help.py +6 -3
- easterobot/commands/info.py +61 -0
- easterobot/commands/reset.py +2 -1
- easterobot/commands/search.py +17 -40
- easterobot/config.py +72 -2
- easterobot/games/connect.py +8 -7
- easterobot/games/game.py +105 -34
- easterobot/games/rock_paper_scissor.py +22 -8
- easterobot/games/tic_tac_toe.py +8 -7
- easterobot/hunts/hunt.py +183 -84
- easterobot/hunts/luck.py +53 -0
- easterobot/query.py +11 -0
- easterobot/resources/config.example.yml +220 -11
- easterobot/utils.py +11 -0
- {easterobot-1.1.1.dist-info → easterobot-1.3.1.dist-info}/METADATA +10 -1
- {easterobot-1.1.1.dist-info → easterobot-1.3.1.dist-info}/RECORD +26 -22
- {easterobot-1.1.1.dist-info → easterobot-1.3.1.dist-info}/WHEEL +0 -0
- {easterobot-1.1.1.dist-info → easterobot-1.3.1.dist-info}/entry_points.txt +0 -0
- {easterobot-1.1.1.dist-info → easterobot-1.3.1.dist-info}/licenses/LICENSE +0 -0
easterobot/games/game.py
CHANGED
@@ -1,8 +1,6 @@
|
|
1
1
|
"""Base class for game."""
|
2
2
|
|
3
3
|
import asyncio
|
4
|
-
import contextlib
|
5
|
-
import datetime
|
6
4
|
import logging
|
7
5
|
from collections.abc import Coroutine
|
8
6
|
from dataclasses import dataclass
|
@@ -12,11 +10,11 @@ from uuid import uuid4
|
|
12
10
|
import discord
|
13
11
|
from discord.ext import commands
|
14
12
|
from discord.message import convert_emoji_reaction
|
15
|
-
from discord.utils import format_dt
|
16
13
|
|
17
14
|
from easterobot.bot import Easterobot
|
18
15
|
from easterobot.commands.base import Context, Interaction, InteractionChannel
|
19
16
|
from easterobot.config import RAND
|
17
|
+
from easterobot.utils import in_seconds
|
20
18
|
|
21
19
|
logger = logging.getLogger(__name__)
|
22
20
|
AsyncCallback = Callable[[], Coroutine[Any, Any, None]]
|
@@ -59,8 +57,11 @@ class Game:
|
|
59
57
|
self._cleanup: Optional[AsyncCallback] = None
|
60
58
|
self._completion: Optional[AsyncCallback] = None
|
61
59
|
self._end_event = asyncio.Event()
|
60
|
+
|
61
|
+
# timeout
|
62
62
|
self._reset_countdown_event = asyncio.Event()
|
63
63
|
self._timeout_task: Optional[asyncio.Task[None]] = None
|
64
|
+
self._timeout_lock: asyncio.Lock = asyncio.Lock()
|
64
65
|
|
65
66
|
async def set_completion(self, callback: AsyncCallback) -> None:
|
66
67
|
"""Get the current state for a player."""
|
@@ -94,20 +95,33 @@ class Game:
|
|
94
95
|
await self._completion()
|
95
96
|
self._end_event.set()
|
96
97
|
|
97
|
-
def start_timer(self, seconds: float) -> str:
|
98
|
+
async def start_timer(self, seconds: float) -> str:
|
98
99
|
"""Start the timer for turn."""
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
100
|
+
async with self._timeout_lock:
|
101
|
+
logger.info(
|
102
|
+
"Start timer of %s seconds for %s",
|
103
|
+
seconds,
|
104
|
+
self,
|
105
|
+
)
|
106
|
+
self._timeout_task = asyncio.create_task(
|
107
|
+
self._timeout_worker(seconds)
|
108
|
+
)
|
109
|
+
return in_seconds(seconds)
|
103
110
|
|
104
111
|
async def stop_timer(self) -> None:
|
105
112
|
"""Stop the timer and wait it end."""
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
113
|
+
async with self._timeout_lock:
|
114
|
+
logger.info("Stop timer for %s", self)
|
115
|
+
if (
|
116
|
+
self._timeout_task
|
117
|
+
and not self._timeout_task.done()
|
118
|
+
and not self._timeout_task.cancelled()
|
119
|
+
):
|
120
|
+
self._reset_countdown_event.set()
|
121
|
+
await self._timeout_task
|
122
|
+
self._timeout_task = None
|
123
|
+
self._reset_countdown_event = asyncio.Event()
|
124
|
+
logger.info("Timer stopped for %s", self)
|
111
125
|
|
112
126
|
async def _timeout_worker(self, seconds: float) -> None:
|
113
127
|
"""Timeout action."""
|
@@ -117,13 +131,14 @@ class Game:
|
|
117
131
|
except asyncio.TimeoutError:
|
118
132
|
if not event.is_set():
|
119
133
|
async with self.lock:
|
134
|
+
logger.info("Timeout for %s", self)
|
120
135
|
await self.on_timeout()
|
121
136
|
|
122
137
|
def __repr__(self) -> str:
|
123
138
|
"""Get game representation."""
|
124
139
|
return (
|
125
140
|
f"<{self.__class__.__qualname__} "
|
126
|
-
f"id={self.id!r} message={self.message!r} "
|
141
|
+
f"id={str(self.id)!r} message={self.message.id!r} "
|
127
142
|
f"terminate={self.terminate!r} winner={self.winner!r}"
|
128
143
|
">"
|
129
144
|
)
|
@@ -152,13 +167,12 @@ class GameCog(commands.Cog):
|
|
152
167
|
from easterobot.games.tic_tac_toe import TicTacToe
|
153
168
|
|
154
169
|
cls = RAND.choice([Connect4, TicTacToe, RockPaperScissor])
|
155
|
-
now = datetime.datetime.now() + datetime.timedelta(seconds=63) # noqa: DTZ005
|
156
|
-
dt = format_dt(now, style="R")
|
157
170
|
msg = await channel.send(
|
158
|
-
f"{user1.mention} et {user2.mention}
|
171
|
+
f"{user1.mention} et {user2.mention} "
|
172
|
+
f"vont s'affronter {in_seconds(121)} ...",
|
159
173
|
reference=reference,
|
160
174
|
)
|
161
|
-
await asyncio.sleep(
|
175
|
+
await asyncio.sleep(121)
|
162
176
|
game: Game = cls(user1, user2, msg) # type: ignore[operator]
|
163
177
|
await self.run(game)
|
164
178
|
return await game.wait_winner()
|
@@ -172,7 +186,13 @@ class GameCog(commands.Cog):
|
|
172
186
|
del self._games[message_id]
|
173
187
|
else:
|
174
188
|
logger.warning("Missing game: %s", game)
|
175
|
-
|
189
|
+
try:
|
190
|
+
await game.message.clear_reactions()
|
191
|
+
except discord.Forbidden:
|
192
|
+
logger.warning(
|
193
|
+
"Missing permission for remove all reactions from %s",
|
194
|
+
message_id,
|
195
|
+
)
|
176
196
|
|
177
197
|
self._games[message_id] = game
|
178
198
|
game._cleanup = _cleanup # noqa: SLF001
|
@@ -186,8 +206,8 @@ class GameCog(commands.Cog):
|
|
186
206
|
bet: int,
|
187
207
|
) -> Optional[discord.Message]:
|
188
208
|
"""Send basic message for initialization."""
|
189
|
-
future: asyncio.Future[bool] = asyncio.Future()
|
190
|
-
accept =
|
209
|
+
future: asyncio.Future[Optional[bool]] = asyncio.Future()
|
210
|
+
accept: Optional[bool] = None
|
191
211
|
|
192
212
|
view = discord.ui.View()
|
193
213
|
yes_btn: Button = discord.ui.Button(
|
@@ -198,44 +218,74 @@ class GameCog(commands.Cog):
|
|
198
218
|
)
|
199
219
|
|
200
220
|
async def yes(interaction: Interaction) -> Any:
|
201
|
-
if interaction.user.id == member.id:
|
202
|
-
future.set_result(True)
|
203
221
|
await interaction.response.defer()
|
222
|
+
if not future.done() and interaction.user.id == member.id:
|
223
|
+
future.set_result(True)
|
204
224
|
|
205
225
|
async def no(interaction: Interaction) -> Any:
|
206
|
-
if interaction.user.id == member.id:
|
207
|
-
future.set_result(False)
|
208
226
|
await interaction.response.defer()
|
227
|
+
if not future.done():
|
228
|
+
if interaction.user.id == member.id:
|
229
|
+
future.set_result(False)
|
230
|
+
if interaction.user.id == ctx.user.id:
|
231
|
+
future.set_result(None)
|
209
232
|
|
210
233
|
yes_btn.callback = yes # type: ignore[method-assign,assignment]
|
211
234
|
no_btn.callback = no # type: ignore[method-assign,assignment]
|
212
235
|
view.add_item(yes_btn)
|
213
236
|
view.add_item(no_btn)
|
214
|
-
|
215
|
-
dt = format_dt(now, style="R")
|
237
|
+
seconds = 600
|
216
238
|
result = await ctx.response.send_message(
|
217
239
|
f"{member.mention}, {ctx.user.mention} "
|
218
240
|
f"vous demande en duel pour `{bet}` œufs ⚔️"
|
219
|
-
f"\nVous devez repondre {
|
241
|
+
f"\nVous devez repondre {in_seconds(seconds)} !",
|
220
242
|
view=view,
|
221
243
|
)
|
222
244
|
message = result.resource
|
223
245
|
if not isinstance(message, discord.Message):
|
224
246
|
error_message = f"Invalid kind of message: {message!r}"
|
225
247
|
raise TypeError(error_message)
|
226
|
-
|
227
|
-
accept = await asyncio.wait_for(future, timeout=
|
248
|
+
try:
|
249
|
+
accept = await asyncio.wait_for(future, timeout=seconds)
|
250
|
+
except asyncio.TimeoutError:
|
251
|
+
await message.edit(
|
252
|
+
content=(
|
253
|
+
f"{ctx.user.mention}, "
|
254
|
+
f"{member.mention} n'a pas accepté le duel 🛡️"
|
255
|
+
f"\n-# Ce message disparaîtra {in_seconds(30)}"
|
256
|
+
),
|
257
|
+
delete_after=30,
|
258
|
+
view=None,
|
259
|
+
)
|
260
|
+
return None
|
261
|
+
if accept is None:
|
262
|
+
await message.edit(
|
263
|
+
content=(
|
264
|
+
f"{member.mention}, {ctx.user.mention} a annulé le duel 🛡️"
|
265
|
+
f"\n-# Ce message disparaîtra {in_seconds(30)}"
|
266
|
+
),
|
267
|
+
delete_after=30,
|
268
|
+
view=None,
|
269
|
+
)
|
270
|
+
return None
|
228
271
|
if not accept:
|
229
272
|
await message.edit(
|
230
273
|
content=(
|
231
|
-
f"{
|
274
|
+
f"{ctx.user.mention}, {member.mention} a refusé le duel 🛡️"
|
275
|
+
f"\n-# Ce message disparaîtra {in_seconds(30)}"
|
232
276
|
),
|
277
|
+
delete_after=30,
|
233
278
|
view=None,
|
234
279
|
)
|
235
280
|
return None
|
236
281
|
if not isinstance(result.resource, discord.Message):
|
237
282
|
error_message = f"Invalid kind of message: {result.resource!r}"
|
238
283
|
raise TypeError(error_message)
|
284
|
+
await result.resource.reply(
|
285
|
+
f"{ctx.user.mention}, {member.mention} a accepté le duel ⚔️"
|
286
|
+
f"\n-# Ce message disparaîtra {in_seconds(30)}",
|
287
|
+
delete_after=30,
|
288
|
+
)
|
239
289
|
return result.resource
|
240
290
|
|
241
291
|
@commands.Cog.listener()
|
@@ -249,14 +299,35 @@ class GameCog(commands.Cog):
|
|
249
299
|
return
|
250
300
|
if payload.message_id in self._games:
|
251
301
|
# Use connection for faster remove
|
252
|
-
emoji = convert_emoji_reaction(payload.emoji)
|
253
302
|
game = self._games[payload.message_id]
|
254
303
|
await asyncio.gather(
|
255
|
-
self.
|
304
|
+
self.silent_reaction_remove(
|
256
305
|
payload.channel_id,
|
257
306
|
payload.message_id,
|
258
|
-
emoji,
|
307
|
+
payload.emoji,
|
259
308
|
payload.user_id,
|
260
309
|
),
|
261
310
|
game.on_reaction(payload.user_id, payload.emoji),
|
262
311
|
)
|
312
|
+
|
313
|
+
async def silent_reaction_remove(
|
314
|
+
self,
|
315
|
+
channel_id: int,
|
316
|
+
message_id: int,
|
317
|
+
emoji: discord.PartialEmoji,
|
318
|
+
user_id: int,
|
319
|
+
) -> None:
|
320
|
+
"""Handle reaction."""
|
321
|
+
try:
|
322
|
+
reaction = convert_emoji_reaction(emoji)
|
323
|
+
await self.bot._connection.http.remove_reaction( # noqa: SLF001
|
324
|
+
channel_id,
|
325
|
+
message_id,
|
326
|
+
reaction,
|
327
|
+
user_id,
|
328
|
+
)
|
329
|
+
except discord.Forbidden:
|
330
|
+
logger.warning(
|
331
|
+
"Missing permission for remove reaction from %s",
|
332
|
+
message_id,
|
333
|
+
)
|
@@ -1,5 +1,6 @@
|
|
1
1
|
"""TicTacToe."""
|
2
2
|
|
3
|
+
import asyncio
|
3
4
|
from functools import partial
|
4
5
|
from typing import Optional
|
5
6
|
|
@@ -44,7 +45,7 @@ class RockPaperScissor(Game):
|
|
44
45
|
embed.set_author(
|
45
46
|
name="Partie en cours", icon_url=self.bot.app_emojis["wait"].url
|
46
47
|
)
|
47
|
-
self.view = discord.ui.View()
|
48
|
+
self.view = discord.ui.View(timeout=1800)
|
48
49
|
rock_btn: Button = discord.ui.Button(
|
49
50
|
style=discord.ButtonStyle.gray,
|
50
51
|
emoji=ROCK,
|
@@ -76,7 +77,10 @@ class RockPaperScissor(Game):
|
|
76
77
|
|
77
78
|
# One button has been pressed
|
78
79
|
if update:
|
79
|
-
await
|
80
|
+
await asyncio.gather(
|
81
|
+
self.stop_timer(),
|
82
|
+
interaction.response.defer(),
|
83
|
+
)
|
80
84
|
await self.update()
|
81
85
|
else:
|
82
86
|
await interaction.response.send_message(
|
@@ -93,12 +97,18 @@ class RockPaperScissor(Game):
|
|
93
97
|
self.view.add_item(scissor_btn)
|
94
98
|
await self.update()
|
95
99
|
|
96
|
-
async def update(self) -> None: # noqa: PLR0912, PLR0915
|
100
|
+
async def update(self) -> None: # noqa: C901, PLR0912, PLR0915
|
97
101
|
"""Update the current display."""
|
98
102
|
embed = discord.Embed(color=0xF2BC32)
|
99
103
|
# Both player have played
|
100
104
|
header = "Partie en cours"
|
101
|
-
if self.play1 is None:
|
105
|
+
if self.play1 is None and self.play2 is None:
|
106
|
+
icon_url = self.bot.app_emojis["wait"].url
|
107
|
+
info = (
|
108
|
+
f"En attente de {self.player1.mention} "
|
109
|
+
f"et {self.player2.mention} ..."
|
110
|
+
)
|
111
|
+
elif self.play1 is None:
|
102
112
|
icon_url = self.player1.display_avatar.url
|
103
113
|
info = f"En attente de {self.player1.mention} ..."
|
104
114
|
elif self.play2 is None:
|
@@ -120,10 +130,10 @@ class RockPaperScissor(Game):
|
|
120
130
|
for play1, play2 in self.history:
|
121
131
|
i1 = EMOJIS.index(play1)
|
122
132
|
i2 = EMOJIS.index(play2)
|
123
|
-
if i1 == (i2
|
133
|
+
if i1 == (i2 - 1) % 3:
|
124
134
|
pt1 += 1
|
125
135
|
text = self.player1.mention
|
126
|
-
elif i1 == (i2
|
136
|
+
elif i1 == (i2 + 1) % 3:
|
127
137
|
pt2 += 1
|
128
138
|
text = self.player2.mention
|
129
139
|
else:
|
@@ -166,10 +176,14 @@ class RockPaperScissor(Game):
|
|
166
176
|
if not self.timeout:
|
167
177
|
await self.set_winner(final_winner)
|
168
178
|
else:
|
169
|
-
dt = self.start_timer(
|
179
|
+
dt = await self.start_timer(31)
|
170
180
|
embed.description += f"\n\n{info}\n\nFin du tour {dt}"
|
171
181
|
embed.set_author(name=header, icon_url=icon_url)
|
172
|
-
await self.message.edit(
|
182
|
+
await self.message.edit(
|
183
|
+
embed=embed,
|
184
|
+
view=self.view,
|
185
|
+
content=(f"-# {self.player1.mention} {self.player2.mention}"),
|
186
|
+
)
|
173
187
|
|
174
188
|
def compute_winner(
|
175
189
|
self, play1: str, play2: str
|
easterobot/games/tic_tac_toe.py
CHANGED
@@ -1,13 +1,12 @@
|
|
1
1
|
"""TicTacToe."""
|
2
2
|
|
3
|
-
import datetime
|
4
3
|
from typing import Optional
|
5
4
|
|
6
5
|
import discord
|
7
|
-
from discord.utils import format_dt
|
8
6
|
from typing_extensions import override
|
9
7
|
|
10
8
|
from easterobot.games.game import Game, Player
|
9
|
+
from easterobot.utils import in_seconds
|
11
10
|
|
12
11
|
EMOJIS_MAPPER = {
|
13
12
|
"1️⃣": 0,
|
@@ -44,7 +43,7 @@ class TicTacToe(Game):
|
|
44
43
|
await self.update()
|
45
44
|
for emoji in EMOJIS:
|
46
45
|
await self.message.add_reaction(emoji)
|
47
|
-
self.start_timer(
|
46
|
+
await self.start_timer(31)
|
48
47
|
|
49
48
|
async def update(self) -> None:
|
50
49
|
"""Update the message."""
|
@@ -85,8 +84,7 @@ class TicTacToe(Game):
|
|
85
84
|
content += footer
|
86
85
|
|
87
86
|
if not self.terminate:
|
88
|
-
|
89
|
-
content += f"\n\nFin du tour {format_dt(now, style='R')}"
|
87
|
+
content += f"\n\nFin du tour {in_seconds(31)}"
|
90
88
|
|
91
89
|
embed = discord.Embed(description=content, color=self.color(user))
|
92
90
|
embed.set_author(
|
@@ -99,7 +97,10 @@ class TicTacToe(Game):
|
|
99
97
|
)
|
100
98
|
self.message = await self.message.edit(
|
101
99
|
embed=embed,
|
102
|
-
content=
|
100
|
+
content=(
|
101
|
+
f"-# {self.player1.member.mention} "
|
102
|
+
f"{self.player2.member.mention}"
|
103
|
+
),
|
103
104
|
view=None,
|
104
105
|
)
|
105
106
|
|
@@ -142,7 +143,7 @@ class TicTacToe(Game):
|
|
142
143
|
await self.set_winner(None)
|
143
144
|
else:
|
144
145
|
self.turn += 1
|
145
|
-
self.start_timer(
|
146
|
+
await self.start_timer(31)
|
146
147
|
await self.update()
|
147
148
|
|
148
149
|
@override
|