easterobot 1.3.2__py3-none-any.whl → 1.5.2__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 +14 -1
- easterobot/casino/__init__.py +1 -0
- easterobot/casino/roulette.py +269 -0
- easterobot/commands/__init__.py +2 -0
- easterobot/commands/game.py +131 -118
- easterobot/commands/reset.py +11 -14
- easterobot/commands/roulette.py +34 -0
- easterobot/commands/top.py +73 -65
- easterobot/config.py +35 -8
- easterobot/games/{connect.py → connect4.py} +25 -28
- easterobot/games/game.py +126 -54
- easterobot/games/rock_paper_scissor.py +33 -30
- easterobot/games/skyjo.py +805 -0
- easterobot/games/tic_tac_toe.py +19 -18
- easterobot/hunts/hunt.py +49 -18
- easterobot/hunts/rank.py +24 -2
- easterobot/info.py +17 -7
- easterobot/locker.py +180 -0
- easterobot/models.py +9 -0
- easterobot/resources/config.example.yml +8 -2
- easterobot/resources/credits.txt +2 -0
- easterobot/resources/emotes/placements/s1.png +0 -0
- easterobot/resources/emotes/placements/s10.png +0 -0
- easterobot/resources/emotes/placements/s11.png +0 -0
- easterobot/resources/emotes/placements/s12.png +0 -0
- easterobot/resources/emotes/placements/s2.png +0 -0
- easterobot/resources/emotes/placements/s3.png +0 -0
- easterobot/resources/emotes/placements/s4.png +0 -0
- easterobot/resources/emotes/placements/s5.png +0 -0
- easterobot/resources/emotes/placements/s6.png +0 -0
- easterobot/resources/emotes/placements/s7.png +0 -0
- easterobot/resources/emotes/placements/s8.png +0 -0
- easterobot/resources/emotes/placements/s9.png +0 -0
- easterobot/resources/emotes/placements/sA.png +0 -0
- easterobot/resources/emotes/placements/sB.png +0 -0
- easterobot/resources/emotes/placements/sC.png +0 -0
- easterobot/resources/emotes/placements/sD.png +0 -0
- easterobot/resources/emotes/placements/sE.png +0 -0
- easterobot/resources/emotes/placements/sF.png +0 -0
- easterobot/resources/emotes/placements/sG.png +0 -0
- easterobot/resources/emotes/placements/sH.png +0 -0
- easterobot/resources/emotes/placements/sI.png +0 -0
- easterobot/resources/emotes/placements/sJ.png +0 -0
- easterobot/resources/emotes/placements/sK.png +0 -0
- easterobot/resources/emotes/placements/sL.png +0 -0
- easterobot/resources/emotes/placements/sM.png +0 -0
- easterobot/resources/emotes/placements/sN.png +0 -0
- easterobot/resources/emotes/placements/sO.png +0 -0
- easterobot/resources/emotes/placements/sP.png +0 -0
- easterobot/resources/emotes/placements/sQ.png +0 -0
- easterobot/resources/emotes/placements/sR.png +0 -0
- easterobot/resources/emotes/placements/sS.png +0 -0
- easterobot/resources/emotes/placements/sT.png +0 -0
- easterobot/resources/emotes/placements/sU.png +0 -0
- easterobot/resources/emotes/placements/sV.png +0 -0
- easterobot/resources/emotes/placements/sW.png +0 -0
- easterobot/resources/emotes/placements/sX.png +0 -0
- easterobot/resources/emotes/placements/sY.png +0 -0
- easterobot/resources/emotes/placements/sZ.png +0 -0
- easterobot/resources/emotes/placements/s_.png +0 -0
- easterobot/resources/emotes/skyjo/skyjo_back.png +0 -0
- easterobot/resources/emotes/skyjo/skyjo_m1.png +0 -0
- easterobot/resources/emotes/skyjo/skyjo_m2.png +0 -0
- easterobot/resources/emotes/skyjo/skyjo_p0.png +0 -0
- easterobot/resources/emotes/skyjo/skyjo_p1.png +0 -0
- easterobot/resources/emotes/skyjo/skyjo_p10.png +0 -0
- easterobot/resources/emotes/skyjo/skyjo_p11.png +0 -0
- easterobot/resources/emotes/skyjo/skyjo_p12.png +0 -0
- easterobot/resources/emotes/skyjo/skyjo_p2.png +0 -0
- easterobot/resources/emotes/skyjo/skyjo_p3.png +0 -0
- easterobot/resources/emotes/skyjo/skyjo_p4.png +0 -0
- easterobot/resources/emotes/skyjo/skyjo_p5.png +0 -0
- easterobot/resources/emotes/skyjo/skyjo_p6.png +0 -0
- easterobot/resources/emotes/skyjo/skyjo_p7.png +0 -0
- easterobot/resources/emotes/skyjo/skyjo_p8.png +0 -0
- easterobot/resources/emotes/skyjo/skyjo_p9.png +0 -0
- {easterobot-1.3.2.dist-info → easterobot-1.5.2.dist-info}/METADATA +23 -19
- easterobot-1.5.2.dist-info/RECORD +130 -0
- easterobot-1.3.2.dist-info/RECORD +0 -70
- {easterobot-1.3.2.dist-info → easterobot-1.5.2.dist-info}/WHEEL +0 -0
- {easterobot-1.3.2.dist-info → easterobot-1.5.2.dist-info}/entry_points.txt +0 -0
- {easterobot-1.3.2.dist-info → easterobot-1.5.2.dist-info}/licenses/LICENSE +0 -0
easterobot/games/tic_tac_toe.py
CHANGED
@@ -1,10 +1,12 @@
|
|
1
1
|
"""TicTacToe."""
|
2
2
|
|
3
|
+
import asyncio
|
3
4
|
from typing import Optional
|
4
5
|
|
5
6
|
import discord
|
6
7
|
from typing_extensions import override
|
7
8
|
|
9
|
+
from easterobot.bot import Easterobot
|
8
10
|
from easterobot.games.game import Game, Player
|
9
11
|
from easterobot.utils import in_seconds
|
10
12
|
|
@@ -25,25 +27,24 @@ EMOJIS = tuple(EMOJIS_MAPPER)
|
|
25
27
|
class TicTacToe(Game):
|
26
28
|
def __init__(
|
27
29
|
self,
|
28
|
-
|
29
|
-
player2: discord.Member,
|
30
|
+
bot: Easterobot,
|
30
31
|
message: discord.Message,
|
32
|
+
*members: discord.Member,
|
31
33
|
) -> None:
|
32
34
|
"""Initialize grid."""
|
33
35
|
self.grid: list[Optional[Player]] = [None] * 9
|
34
36
|
self.timeout = False
|
35
|
-
self.player1 = Player(player1, 1)
|
36
|
-
self.player2 = Player(player2, 2)
|
37
37
|
self.turn = 0
|
38
|
-
super().__init__(message)
|
38
|
+
super().__init__(bot, message, *members)
|
39
39
|
|
40
40
|
@override
|
41
41
|
async def on_start(self) -> None:
|
42
42
|
"""Run."""
|
43
43
|
await self.update()
|
44
|
+
await self.start_timer(31)
|
44
45
|
for emoji in EMOJIS:
|
46
|
+
await asyncio.sleep(0.1)
|
45
47
|
await self.message.add_reaction(emoji)
|
46
|
-
await self.start_timer(31)
|
47
48
|
|
48
49
|
async def update(self) -> None:
|
49
50
|
"""Update the message."""
|
@@ -55,13 +56,13 @@ class TicTacToe(Game):
|
|
55
56
|
user: Optional[Player] = self.current
|
56
57
|
elif self.winner:
|
57
58
|
forfait = "par forfait " if self.timeout else ""
|
58
|
-
footer = f"\n## Gagnant {forfait}{self.winner.mention} 🎉"
|
59
|
+
footer = f"\n## Gagnant {forfait}{self.winner.member.mention} 🎉"
|
59
60
|
user = self.current
|
60
61
|
else:
|
61
62
|
footer = (
|
62
63
|
"\n## Égalité entre "
|
63
|
-
f"{self.
|
64
|
-
f"et {self.
|
64
|
+
f"{self.players[0].member.mention} "
|
65
|
+
f"et {self.players[1].member.mention} 🤝"
|
65
66
|
)
|
66
67
|
user = None
|
67
68
|
|
@@ -72,9 +73,9 @@ class TicTacToe(Game):
|
|
72
73
|
for col in range(3):
|
73
74
|
index = row * 3 + col
|
74
75
|
player = self.grid[index]
|
75
|
-
if player == self.
|
76
|
+
if player == self.players[0]:
|
76
77
|
piece = "❌"
|
77
|
-
elif player == self.
|
78
|
+
elif player == self.players[1]:
|
78
79
|
piece = "⭕"
|
79
80
|
else:
|
80
81
|
piece = EMOJIS[index]
|
@@ -98,8 +99,8 @@ class TicTacToe(Game):
|
|
98
99
|
self.message = await self.message.edit(
|
99
100
|
embed=embed,
|
100
101
|
content=(
|
101
|
-
f"-# {self.
|
102
|
-
f"{self.
|
102
|
+
f"-# {self.players[0].member.mention} "
|
103
|
+
f"{self.players[1].member.mention}"
|
103
104
|
),
|
104
105
|
view=None,
|
105
106
|
)
|
@@ -116,15 +117,15 @@ class TicTacToe(Game):
|
|
116
117
|
@property
|
117
118
|
def current(self) -> Player:
|
118
119
|
"""Get current member."""
|
119
|
-
return [self.
|
120
|
+
return [self.players[0], self.players[1]][self.turn % 2]
|
120
121
|
|
121
122
|
def color(self, player: Optional[Player]) -> Optional[discord.Colour]:
|
122
123
|
"""Color of the embed."""
|
123
124
|
if player is None:
|
124
125
|
return discord.Colour.from_str("#d4d5d6")
|
125
|
-
if player == self.
|
126
|
+
if player == self.players[0]:
|
126
127
|
return discord.Colour.from_str("#F17720")
|
127
|
-
if player == self.
|
128
|
+
if player == self.players[1]:
|
128
129
|
return discord.Colour.from_str("#0474BA")
|
129
130
|
error_message = f"Invalid player: {player!r}"
|
130
131
|
raise ValueError(error_message)
|
@@ -138,7 +139,7 @@ class TicTacToe(Game):
|
|
138
139
|
self.grid[index] = player
|
139
140
|
|
140
141
|
if self._is_winner(player):
|
141
|
-
await self.set_winner(player
|
142
|
+
await self.set_winner(player)
|
142
143
|
elif all(cell is not None for cell in self.grid):
|
143
144
|
await self.set_winner(None)
|
144
145
|
else:
|
@@ -150,7 +151,7 @@ class TicTacToe(Game):
|
|
150
151
|
async def on_timeout(self) -> None:
|
151
152
|
self.turn += 1
|
152
153
|
self.timeout = True
|
153
|
-
await self.set_winner(self.current
|
154
|
+
await self.set_winner(self.current)
|
154
155
|
await self.update()
|
155
156
|
|
156
157
|
def _is_winner(self, player: Player) -> bool:
|
easterobot/hunts/hunt.py
CHANGED
@@ -17,6 +17,7 @@ from sqlalchemy import and_, func, select, update
|
|
17
17
|
from sqlalchemy.ext.asyncio import AsyncSession
|
18
18
|
|
19
19
|
from easterobot.bot import Easterobot
|
20
|
+
from easterobot.casino.roulette import RouletteManager
|
20
21
|
from easterobot.config import (
|
21
22
|
RAND,
|
22
23
|
agree,
|
@@ -24,6 +25,7 @@ from easterobot.config import (
|
|
24
25
|
from easterobot.hunts.luck import HuntLuck
|
25
26
|
from easterobot.models import Egg, Hunt
|
26
27
|
from easterobot.query import QueryManager
|
28
|
+
from easterobot.utils import in_seconds
|
27
29
|
|
28
30
|
logger = logging.getLogger(__name__)
|
29
31
|
DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
|
@@ -98,7 +100,7 @@ class HuntQuery(QueryManager):
|
|
98
100
|
)
|
99
101
|
return egg_max or 0
|
100
102
|
|
101
|
-
async def
|
103
|
+
async def get_egg_count(
|
102
104
|
self,
|
103
105
|
session: AsyncSession,
|
104
106
|
guild_id: int,
|
@@ -125,7 +127,7 @@ class HuntQuery(QueryManager):
|
|
125
127
|
) -> HuntLuck:
|
126
128
|
"""Get the luck of a member."""
|
127
129
|
luck = 1.0
|
128
|
-
egg_count = await self.
|
130
|
+
egg_count = await self.get_egg_count(session, guild_id, user_id)
|
129
131
|
if egg_count != 0:
|
130
132
|
egg_max = await self.get_max_eggs(session, guild_id)
|
131
133
|
if egg_max != 0:
|
@@ -147,6 +149,9 @@ class HuntCog(commands.Cog, HuntQuery):
|
|
147
149
|
@commands.Cog.listener()
|
148
150
|
async def on_ready(self) -> None:
|
149
151
|
"""Handle ready event, can be trigger many time if disconnected."""
|
152
|
+
# Wait main finish
|
153
|
+
await self.bot.init_finished.wait()
|
154
|
+
|
150
155
|
# Unlock all eggs
|
151
156
|
logger.info("Unlock all previous eggs")
|
152
157
|
async with AsyncSession(self.bot.engine) as session:
|
@@ -155,6 +160,7 @@ class HuntCog(commands.Cog, HuntQuery):
|
|
155
160
|
|
156
161
|
# Start hunt
|
157
162
|
logger.info("Start hunt handler")
|
163
|
+
# TODO(dashstrom): rework this
|
158
164
|
pending_hunts: set[asyncio.Task[Any]] = set()
|
159
165
|
while True:
|
160
166
|
if pending_hunts:
|
@@ -173,6 +179,7 @@ class HuntCog(commands.Cog, HuntQuery):
|
|
173
179
|
description: str,
|
174
180
|
*,
|
175
181
|
member_id: Optional[int] = None,
|
182
|
+
casino: bool = False,
|
176
183
|
send_method: Optional[
|
177
184
|
Callable[..., Awaitable[discord.Message]]
|
178
185
|
] = None,
|
@@ -184,6 +191,14 @@ class HuntCog(commands.Cog, HuntQuery):
|
|
184
191
|
return
|
185
192
|
guild = channel.guild
|
186
193
|
|
194
|
+
# Random casino events
|
195
|
+
if casino:
|
196
|
+
casino_event = self.bot.config.casino.sample_event()
|
197
|
+
if casino_event:
|
198
|
+
manager = RouletteManager(self.bot)
|
199
|
+
await manager.run(channel)
|
200
|
+
return
|
201
|
+
|
187
202
|
# Get from config
|
188
203
|
action = self.bot.config.action.rand()
|
189
204
|
emoji = self.bot.egg_emotes.rand()
|
@@ -200,6 +215,7 @@ class HuntCog(commands.Cog, HuntQuery):
|
|
200
215
|
# Start hunt
|
201
216
|
logger.info("Start hunt in %s", channel.jump_url)
|
202
217
|
timeout = self.bot.config.hunt.timeout + 1
|
218
|
+
has_game = False
|
203
219
|
view = discord.ui.View(timeout=timeout)
|
204
220
|
button: discord.ui.Button[Any] = discord.ui.Button(
|
205
221
|
label=label,
|
@@ -253,14 +269,11 @@ class HuntCog(commands.Cog, HuntQuery):
|
|
253
269
|
# Set the button callback
|
254
270
|
button.callback = button_callback # type: ignore[method-assign]
|
255
271
|
|
256
|
-
# Set next hunt
|
257
|
-
next_hunt = time.time() + timeout
|
258
|
-
|
259
272
|
# Create and embed
|
260
273
|
emb = embed(
|
261
274
|
title="Un œuf a été découvert !",
|
262
275
|
description=description
|
263
|
-
+ f"\n\
|
276
|
+
+ f"\n\n-# Tirage du vainqueur {in_seconds(timeout)}",
|
264
277
|
thumbnail=emoji.url,
|
265
278
|
)
|
266
279
|
|
@@ -269,18 +282,18 @@ class HuntCog(commands.Cog, HuntQuery):
|
|
269
282
|
message = await channel.send(embed=emb, view=view)
|
270
283
|
else:
|
271
284
|
message = await send_method(embed=emb, view=view)
|
285
|
+
message = await message.channel.fetch_message(message.id)
|
272
286
|
|
273
287
|
# TODO(dashstrom): channel is wrong due to the send message !
|
274
288
|
# TODO(dashstrom): Why wait if timeout ???
|
275
289
|
# Wait the end of the hunt
|
276
290
|
message_url = f"{channel.jump_url}/{message.id}"
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
logger.info("End hunt for %s", message_url)
|
291
|
+
try:
|
292
|
+
await asyncio.wait_for(
|
293
|
+
view.wait(), timeout=self.bot.config.hunt.timeout
|
294
|
+
)
|
295
|
+
except asyncio.TimeoutError:
|
296
|
+
logger.info("End hunt for %s", message_url)
|
284
297
|
|
285
298
|
# Disable button and view after hunt
|
286
299
|
button.disabled = True
|
@@ -315,6 +328,7 @@ class HuntCog(commands.Cog, HuntQuery):
|
|
315
328
|
|
316
329
|
if RAND.random() < self.bot.config.hunt.game:
|
317
330
|
# Update button
|
331
|
+
has_game = True
|
318
332
|
button.label = "Duel en cours ..."
|
319
333
|
button.style = discord.ButtonStyle.gray
|
320
334
|
|
@@ -329,7 +343,7 @@ class HuntCog(commands.Cog, HuntQuery):
|
|
329
343
|
if duel_winner is None:
|
330
344
|
winner = None
|
331
345
|
loser = None
|
332
|
-
elif duel_winner == loser:
|
346
|
+
elif duel_winner.member == loser:
|
333
347
|
winner, loser = loser, winner
|
334
348
|
|
335
349
|
if winner:
|
@@ -358,7 +372,11 @@ class HuntCog(commands.Cog, HuntQuery):
|
|
358
372
|
)
|
359
373
|
emb = embed(
|
360
374
|
title=text,
|
361
|
-
description=
|
375
|
+
description=(
|
376
|
+
action.fail.text(loser)
|
377
|
+
+ "\n\n-# Ce message disparaîtra "
|
378
|
+
+ in_seconds(300)
|
379
|
+
),
|
362
380
|
image=action.fail.gif,
|
363
381
|
)
|
364
382
|
await channel.send(
|
@@ -368,8 +386,17 @@ class HuntCog(commands.Cog, HuntQuery):
|
|
368
386
|
)
|
369
387
|
|
370
388
|
if winner:
|
371
|
-
#
|
372
|
-
|
389
|
+
# Get egg count (or the new counter if game)
|
390
|
+
if has_game:
|
391
|
+
winner_eggs = await self.get_egg_count(
|
392
|
+
session,
|
393
|
+
winner.guild.id,
|
394
|
+
winner.id,
|
395
|
+
)
|
396
|
+
else:
|
397
|
+
winner_eggs = eggs.get(winner.id, 0) + 1
|
398
|
+
|
399
|
+
# Send the winner embed winner
|
373
400
|
emb = embed(
|
374
401
|
title=f"{winner.display_name} récupère un œuf",
|
375
402
|
description=action.success.text(winner),
|
@@ -488,7 +515,11 @@ class HuntCog(commands.Cog, HuntQuery):
|
|
488
515
|
try:
|
489
516
|
await asyncio.gather(
|
490
517
|
*[
|
491
|
-
self.start_hunt(
|
518
|
+
self.start_hunt(
|
519
|
+
hunt_id,
|
520
|
+
self.bot.config.appear.rand(),
|
521
|
+
casino=True,
|
522
|
+
)
|
492
523
|
for hunt_id in hunt_ids
|
493
524
|
]
|
494
525
|
)
|
easterobot/hunts/rank.py
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
from dataclasses import dataclass
|
4
4
|
|
5
|
-
from sqlalchemy import func, select
|
5
|
+
from sqlalchemy import and_, func, not_, select
|
6
6
|
from sqlalchemy.ext.asyncio import AsyncSession
|
7
7
|
|
8
8
|
from easterobot.config import agree
|
@@ -51,6 +51,23 @@ class Ranking:
|
|
51
51
|
return []
|
52
52
|
return self.hunters[limit * n : limit * (n + 1)]
|
53
53
|
|
54
|
+
def count_page(self, n: int) -> int:
|
55
|
+
"""Count the number of page.
|
56
|
+
|
57
|
+
Example:
|
58
|
+
>>> Ranking([Hunter(1, 1, 3), Hunter(2, 2, 2)]).count_page(10)
|
59
|
+
1
|
60
|
+
>>> Ranking([]).count_page(10)
|
61
|
+
0
|
62
|
+
>>> Ranking([Hunter(i, i, 20 - i) for i in range(10)]).count_page(10)
|
63
|
+
1
|
64
|
+
>>> Ranking([Hunter(i, i, 20 - i) for i in range(11)]).count_page(10)
|
65
|
+
2
|
66
|
+
"""
|
67
|
+
if not self.hunters:
|
68
|
+
return 0
|
69
|
+
return (len(self.hunters) - 1) // n + 1
|
70
|
+
|
54
71
|
def get(self, member_id: int) -> Hunter:
|
55
72
|
"""Get a hunter."""
|
56
73
|
for hunter in self.hunters:
|
@@ -62,15 +79,20 @@ class Ranking:
|
|
62
79
|
async def from_guild(
|
63
80
|
session: AsyncSession,
|
64
81
|
guild_id: int,
|
82
|
+
*,
|
83
|
+
unlock_only: bool = False,
|
65
84
|
) -> "Ranking":
|
66
85
|
"""Get ranks by page."""
|
86
|
+
where = Egg.guild_id == guild_id
|
87
|
+
if unlock_only:
|
88
|
+
where = and_(where, not_(Egg.lock))
|
67
89
|
query = (
|
68
90
|
select(
|
69
91
|
Egg.user_id,
|
70
92
|
func.rank().over(order_by=func.count().desc()).label("row"),
|
71
93
|
func.count().label("count"),
|
72
94
|
)
|
73
|
-
.where(
|
95
|
+
.where(where)
|
74
96
|
.group_by(Egg.user_id)
|
75
97
|
.order_by(func.count().desc())
|
76
98
|
)
|
easterobot/info.py
CHANGED
@@ -1,18 +1,28 @@
|
|
1
1
|
"""Module holding metadata."""
|
2
2
|
|
3
|
+
import logging
|
3
4
|
from importlib.metadata import Distribution
|
4
5
|
|
6
|
+
logger = logging.getLogger(__name__)
|
5
7
|
_DISTRIBUTION = Distribution.from_name(
|
6
8
|
"easterobot",
|
7
9
|
)
|
8
10
|
_METADATA = _DISTRIBUTION.metadata
|
9
|
-
|
10
|
-
if "Author" in _METADATA:
|
11
|
-
|
12
|
-
|
11
|
+
if len(_METADATA) != 0:
|
12
|
+
if "Author" in _METADATA:
|
13
|
+
__author__ = str(_METADATA["Author"])
|
14
|
+
__email__ = str(_METADATA["Author-email"])
|
15
|
+
else:
|
16
|
+
__author__, __email__ = _METADATA["Author-email"][:-1].split(" <", 1)
|
17
|
+
__version__ = _METADATA["Version"]
|
18
|
+
__summary__ = _METADATA["Summary"]
|
13
19
|
else:
|
14
|
-
|
15
|
-
|
16
|
-
|
20
|
+
logger.warning("Cannot load package metadata, please reinstall !")
|
21
|
+
|
22
|
+
__author__ = "Unknown"
|
23
|
+
__email__ = "Unknown"
|
24
|
+
__version__ = "Unknown"
|
25
|
+
__summary__ = "Unknown"
|
26
|
+
|
17
27
|
__copyright__ = f"{__author__} <{__email__}>"
|
18
28
|
__issues__ = "https://github.com/Dashstrom/easterobot/issues"
|
easterobot/locker.py
ADDED
@@ -0,0 +1,180 @@
|
|
1
|
+
"""Lock the module."""
|
2
|
+
|
3
|
+
import asyncio
|
4
|
+
import logging
|
5
|
+
from collections.abc import AsyncIterator, Iterable
|
6
|
+
from contextlib import AbstractAsyncContextManager, asynccontextmanager
|
7
|
+
from types import TracebackType
|
8
|
+
from typing import ClassVar, Optional, final
|
9
|
+
|
10
|
+
import discord
|
11
|
+
from sqlalchemy import and_, func, not_, select
|
12
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
13
|
+
from typing_extensions import override
|
14
|
+
|
15
|
+
from easterobot.config import agree
|
16
|
+
from easterobot.models import Egg
|
17
|
+
|
18
|
+
logger = logging.getLogger(__name__)
|
19
|
+
|
20
|
+
|
21
|
+
async def fetch_unlocked_eggs(
|
22
|
+
session: AsyncSession,
|
23
|
+
guild_id: int,
|
24
|
+
user_id: int,
|
25
|
+
counter: int,
|
26
|
+
) -> list[Egg]:
|
27
|
+
"""Get the count of unlocked eggs."""
|
28
|
+
return list(
|
29
|
+
(
|
30
|
+
await session.scalars(
|
31
|
+
select(Egg)
|
32
|
+
.where(
|
33
|
+
and_(
|
34
|
+
Egg.guild_id == guild_id,
|
35
|
+
Egg.user_id == user_id,
|
36
|
+
not_(Egg.lock),
|
37
|
+
)
|
38
|
+
)
|
39
|
+
.order_by(func.random()) # Randomize
|
40
|
+
.limit(counter)
|
41
|
+
)
|
42
|
+
).all()
|
43
|
+
)
|
44
|
+
|
45
|
+
|
46
|
+
async def fetch_unlocked_egg_count(
|
47
|
+
session: AsyncSession,
|
48
|
+
guild_id: int,
|
49
|
+
user_ids: Iterable[int],
|
50
|
+
) -> dict[int, int]:
|
51
|
+
"""Get the count of unlocked eggs."""
|
52
|
+
user_ids = list(user_ids)
|
53
|
+
res = await session.execute(
|
54
|
+
select(Egg.user_id, func.count().label("count"))
|
55
|
+
.where(
|
56
|
+
and_(
|
57
|
+
Egg.guild_id == guild_id,
|
58
|
+
Egg.user_id.in_(user_ids),
|
59
|
+
not_(Egg.lock),
|
60
|
+
)
|
61
|
+
)
|
62
|
+
.group_by(Egg.user_id)
|
63
|
+
)
|
64
|
+
result = dict(res.all()) # type: ignore[arg-type]
|
65
|
+
for user_id in user_ids:
|
66
|
+
if user_id not in result:
|
67
|
+
result[user_id] = 0
|
68
|
+
return result
|
69
|
+
|
70
|
+
|
71
|
+
class EggLockerError(Exception):
|
72
|
+
pass
|
73
|
+
|
74
|
+
|
75
|
+
@final
|
76
|
+
class EggLocker(AbstractAsyncContextManager["EggLocker"]):
|
77
|
+
# TODO(dashstrom): memory leak over time
|
78
|
+
_guild_locks: ClassVar[dict[int, asyncio.Lock]] = {}
|
79
|
+
|
80
|
+
def __init__(self, session: AsyncSession, guild_id: int) -> None:
|
81
|
+
"""Init EggLocker."""
|
82
|
+
self._session = session
|
83
|
+
self._guild_id = guild_id
|
84
|
+
self._eggs: list[Egg] = []
|
85
|
+
|
86
|
+
@asynccontextmanager
|
87
|
+
async def transaction(self) -> AsyncIterator[None]:
|
88
|
+
"""Return guild lock."""
|
89
|
+
if self._guild_id not in self._guild_locks:
|
90
|
+
self._guild_locks[self._guild_id] = asyncio.Lock()
|
91
|
+
async with self._guild_locks[self._guild_id]:
|
92
|
+
yield
|
93
|
+
await self._session.commit()
|
94
|
+
|
95
|
+
async def get(
|
96
|
+
self,
|
97
|
+
member: discord.Member,
|
98
|
+
egg_count: int,
|
99
|
+
) -> list[Egg]:
|
100
|
+
"""Update the egg locker."""
|
101
|
+
eggs = await fetch_unlocked_eggs(
|
102
|
+
self._session,
|
103
|
+
self._guild_id,
|
104
|
+
member.id,
|
105
|
+
egg_count,
|
106
|
+
)
|
107
|
+
if len(eggs) < egg_count:
|
108
|
+
egg_text = agree("œuf", "œufs", len(eggs))
|
109
|
+
error_message = (
|
110
|
+
f"{member.mention} n'a plus que {len(eggs)} {egg_text} "
|
111
|
+
f"disponible sur les {egg_count} demandés"
|
112
|
+
)
|
113
|
+
raise EggLockerError(error_message)
|
114
|
+
for egg in eggs:
|
115
|
+
egg.lock = True
|
116
|
+
self._eggs.extend(eggs)
|
117
|
+
logger.info(
|
118
|
+
"Lock %s egg(s) of %s (%s)",
|
119
|
+
len(eggs),
|
120
|
+
member.name,
|
121
|
+
member.id,
|
122
|
+
)
|
123
|
+
return eggs
|
124
|
+
|
125
|
+
async def delete(self, eggs: Iterable[Egg]) -> None:
|
126
|
+
"""Delete eggs."""
|
127
|
+
futures = []
|
128
|
+
for egg in eggs:
|
129
|
+
self._eggs.remove(egg)
|
130
|
+
futures.append(self._session.delete(egg))
|
131
|
+
await asyncio.gather(*futures)
|
132
|
+
|
133
|
+
def update(self, eggs: Iterable[Egg]) -> None:
|
134
|
+
"""Update eggs."""
|
135
|
+
for egg in eggs:
|
136
|
+
self._eggs.append(egg)
|
137
|
+
self._session.add(egg)
|
138
|
+
|
139
|
+
async def pre_check(
|
140
|
+
self,
|
141
|
+
members: dict[discord.Member, int],
|
142
|
+
) -> None:
|
143
|
+
"""Return the list of invalid user."""
|
144
|
+
if not members:
|
145
|
+
return
|
146
|
+
member_ids = {member.id: member for member in members}
|
147
|
+
counter = await fetch_unlocked_egg_count(
|
148
|
+
self._session,
|
149
|
+
self._guild_id,
|
150
|
+
member_ids,
|
151
|
+
)
|
152
|
+
for user_id, egg_count in counter.items():
|
153
|
+
member = member_ids[user_id]
|
154
|
+
required = members[member]
|
155
|
+
if egg_count < required:
|
156
|
+
egg_text = agree("œuf", "œufs", egg_count)
|
157
|
+
error_message = (
|
158
|
+
f"{member.mention} n'a que {egg_count} {egg_text} "
|
159
|
+
f"disponible sur les {required} demandés"
|
160
|
+
)
|
161
|
+
raise EggLockerError(error_message)
|
162
|
+
|
163
|
+
@override
|
164
|
+
async def __aenter__(self) -> "EggLocker":
|
165
|
+
"""Return `self` upon entering the runtime context."""
|
166
|
+
return self
|
167
|
+
|
168
|
+
@override
|
169
|
+
async def __aexit__(
|
170
|
+
self,
|
171
|
+
exc_type: Optional[type[BaseException]],
|
172
|
+
exc_value: Optional[BaseException],
|
173
|
+
traceback: Optional[TracebackType],
|
174
|
+
) -> Optional[bool]:
|
175
|
+
"""Raise any exception triggered within the runtime context."""
|
176
|
+
async with self.transaction():
|
177
|
+
await self._session.rollback()
|
178
|
+
for egg in self._eggs:
|
179
|
+
egg.lock = False
|
180
|
+
return None
|
easterobot/models.py
CHANGED
@@ -37,6 +37,15 @@ class Egg(Base):
|
|
37
37
|
guild_id = self.guild_id or "@me"
|
38
38
|
return f"{DISCORD_URL}/{guild_id}/{self.channel_id}/{self.id}"
|
39
39
|
|
40
|
+
def duplicate(self) -> "Egg":
|
41
|
+
"""Duplicate the egg."""
|
42
|
+
return Egg(
|
43
|
+
guild_id=self.guild_id,
|
44
|
+
channel_id=self.channel_id,
|
45
|
+
user_id=self.user_id,
|
46
|
+
emoji_id=self.emoji_id,
|
47
|
+
)
|
48
|
+
|
40
49
|
|
41
50
|
class Hunt(Base):
|
42
51
|
__tablename__ = "hunt"
|
@@ -11,11 +11,15 @@ logs:
|
|
11
11
|
database: sqlite+aiosqlite://%(data)s/easterobot.db
|
12
12
|
group: egg
|
13
13
|
sleep:
|
14
|
-
start: '
|
15
|
-
end: '
|
14
|
+
start: '01:00:00'
|
15
|
+
end: '10:00:00'
|
16
16
|
divide_hunt: 2
|
17
17
|
divide_discovered: 2
|
18
18
|
divide_spotted: 1.5
|
19
|
+
casino:
|
20
|
+
probability: 0.05
|
21
|
+
roulette:
|
22
|
+
duration: 300
|
19
23
|
hunt:
|
20
24
|
timeout: 300.0
|
21
25
|
cooldown:
|
@@ -45,6 +49,8 @@ commands:
|
|
45
49
|
cooldown: 120.0
|
46
50
|
tictactoe:
|
47
51
|
cooldown: 120.0
|
52
|
+
skyjo:
|
53
|
+
cooldown: 120.0
|
48
54
|
info:
|
49
55
|
cooldown: 30.0
|
50
56
|
basket:
|
easterobot/resources/credits.txt
CHANGED
@@ -3,3 +3,5 @@ icons/arrow.png https://www.flaticon.com/free-icon/right-arrow_10570067
|
|
3
3
|
icons/end.png https://www.flaticon.com/free-icon/end_5553850
|
4
4
|
icons/versus.png https://www.flaticon.com/free-icon/versus_7960356
|
5
5
|
icons/wait.png https://www.flaticon.com/free-icon/hourglass_3874060
|
6
|
+
skyjo/ https://github.com/Dashstrom/easterobot
|
7
|
+
placements/ https://github.com/Dashstrom/easterobot
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|