easterobot 1.1.2__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 +1 -1
- easterobot/commands/enable.py +1 -1
- easterobot/commands/game.py +23 -21
- easterobot/commands/help.py +3 -3
- easterobot/commands/info.py +61 -0
- easterobot/commands/reset.py +1 -1
- easterobot/commands/search.py +14 -40
- easterobot/config.py +71 -1
- 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 +218 -9
- easterobot/utils.py +11 -0
- {easterobot-1.1.2.dist-info → easterobot-1.3.1.dist-info}/METADATA +9 -5
- {easterobot-1.1.2.dist-info → easterobot-1.3.1.dist-info}/RECORD +25 -21
- {easterobot-1.1.2.dist-info → easterobot-1.3.1.dist-info}/WHEEL +0 -0
- {easterobot-1.1.2.dist-info → easterobot-1.3.1.dist-info}/entry_points.txt +0 -0
- {easterobot-1.1.2.dist-info → easterobot-1.3.1.dist-info}/licenses/LICENSE +0 -0
easterobot/hunts/hunt.py
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
import asyncio
|
4
4
|
import logging
|
5
5
|
import time
|
6
|
-
from collections.abc import Awaitable
|
6
|
+
from collections.abc import Awaitable, Iterable, Sequence
|
7
7
|
from datetime import datetime, timezone
|
8
8
|
from typing import (
|
9
9
|
Any,
|
@@ -21,16 +21,128 @@ from easterobot.config import (
|
|
21
21
|
RAND,
|
22
22
|
agree,
|
23
23
|
)
|
24
|
+
from easterobot.hunts.luck import HuntLuck
|
24
25
|
from easterobot.models import Egg, Hunt
|
26
|
+
from easterobot.query import QueryManager
|
25
27
|
|
26
28
|
logger = logging.getLogger(__name__)
|
27
29
|
DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
|
28
30
|
|
29
31
|
|
30
|
-
class
|
32
|
+
class HuntQuery(QueryManager):
|
33
|
+
async def unlock_all_eggs(self, session: AsyncSession) -> None:
|
34
|
+
"""Unlock all eggs."""
|
35
|
+
await session.execute(update(Egg).where(Egg.lock).values(lock=False))
|
36
|
+
|
37
|
+
async def get_egg_count_for_members(
|
38
|
+
self,
|
39
|
+
session: AsyncSession,
|
40
|
+
guild_id: int,
|
41
|
+
user_ids: Iterable[int],
|
42
|
+
) -> dict[int, int]:
|
43
|
+
"""Get egg count for members."""
|
44
|
+
res = await session.execute(
|
45
|
+
select(Egg.user_id, func.count().label("count"))
|
46
|
+
.where(
|
47
|
+
and_(
|
48
|
+
Egg.guild_id == guild_id,
|
49
|
+
Egg.user_id.in_(user_ids),
|
50
|
+
)
|
51
|
+
)
|
52
|
+
.group_by(Egg.user_id)
|
53
|
+
)
|
54
|
+
return dict(res.all()) # type: ignore[arg-type]
|
55
|
+
|
56
|
+
async def get_hunt(
|
57
|
+
self,
|
58
|
+
session: AsyncSession,
|
59
|
+
guild_id: int,
|
60
|
+
channel_id: int,
|
61
|
+
) -> Optional[Hunt]:
|
62
|
+
"""Get hunt."""
|
63
|
+
hunt = await session.scalar(
|
64
|
+
select(Hunt).where(
|
65
|
+
and_(
|
66
|
+
Hunt.guild_id == guild_id,
|
67
|
+
Hunt.channel_id == channel_id,
|
68
|
+
)
|
69
|
+
)
|
70
|
+
)
|
71
|
+
return hunt # noqa: RET504
|
72
|
+
|
73
|
+
async def get_hunts_after(
|
74
|
+
self,
|
75
|
+
session: AsyncSession,
|
76
|
+
after: float,
|
77
|
+
) -> Sequence[Hunt]:
|
78
|
+
"""Get all hunts after a given datetime."""
|
79
|
+
res = await session.scalars(
|
80
|
+
select(Hunt).where(Hunt.next_egg <= after),
|
81
|
+
)
|
82
|
+
return res.all()
|
83
|
+
|
84
|
+
async def get_max_eggs(
|
85
|
+
self,
|
86
|
+
session: AsyncSession,
|
87
|
+
guild_id: int,
|
88
|
+
) -> int:
|
89
|
+
"""Get the maximum number of eggs."""
|
90
|
+
egg_max = await session.scalar(
|
91
|
+
select(
|
92
|
+
func.count().label("max"),
|
93
|
+
)
|
94
|
+
.where(Egg.guild_id == guild_id)
|
95
|
+
.group_by(Egg.user_id)
|
96
|
+
.order_by(func.count().label("max").desc())
|
97
|
+
.limit(1)
|
98
|
+
)
|
99
|
+
return egg_max or 0
|
100
|
+
|
101
|
+
async def get_eggs(
|
102
|
+
self,
|
103
|
+
session: AsyncSession,
|
104
|
+
guild_id: int,
|
105
|
+
user_id: int,
|
106
|
+
) -> int:
|
107
|
+
"""Get eggs of player."""
|
108
|
+
eggs = await session.scalar(
|
109
|
+
select(func.count().label("count")).where(
|
110
|
+
and_(
|
111
|
+
Egg.guild_id == guild_id,
|
112
|
+
Egg.user_id == user_id,
|
113
|
+
)
|
114
|
+
)
|
115
|
+
)
|
116
|
+
return eggs or 0
|
117
|
+
|
118
|
+
async def get_luck(
|
119
|
+
self,
|
120
|
+
session: AsyncSession,
|
121
|
+
guild_id: int,
|
122
|
+
user_id: int,
|
123
|
+
*,
|
124
|
+
sleep_hours: bool = False,
|
125
|
+
) -> HuntLuck:
|
126
|
+
"""Get the luck of a member."""
|
127
|
+
luck = 1.0
|
128
|
+
egg_count = await self.get_eggs(session, guild_id, user_id)
|
129
|
+
if egg_count != 0:
|
130
|
+
egg_max = await self.get_max_eggs(session, guild_id)
|
131
|
+
if egg_max != 0:
|
132
|
+
luck = 1 - egg_count / egg_max
|
133
|
+
return HuntLuck(
|
134
|
+
egg_count=egg_count,
|
135
|
+
luck=luck,
|
136
|
+
sleep_hours=sleep_hours,
|
137
|
+
config=self.config,
|
138
|
+
)
|
139
|
+
|
140
|
+
|
141
|
+
class HuntCog(commands.Cog, HuntQuery):
|
31
142
|
def __init__(self, bot: Easterobot) -> None:
|
32
143
|
"""Instantiate HuntCog."""
|
33
144
|
self.bot = bot
|
145
|
+
super().__init__(self.bot.config)
|
34
146
|
|
35
147
|
@commands.Cog.listener()
|
36
148
|
async def on_ready(self) -> None:
|
@@ -38,9 +150,7 @@ class HuntCog(commands.Cog):
|
|
38
150
|
# Unlock all eggs
|
39
151
|
logger.info("Unlock all previous eggs")
|
40
152
|
async with AsyncSession(self.bot.engine) as session:
|
41
|
-
await
|
42
|
-
update(Egg).where(Egg.lock).values(lock=False)
|
43
|
-
)
|
153
|
+
await self.unlock_all_eggs(session)
|
44
154
|
await session.commit()
|
45
155
|
|
46
156
|
# Start hunt
|
@@ -150,7 +260,7 @@ class HuntCog(commands.Cog):
|
|
150
260
|
emb = embed(
|
151
261
|
title="Un œuf a été découvert !",
|
152
262
|
description=description
|
153
|
-
+ f"\n\nTirage du
|
263
|
+
+ f"\n\nTirage du vainqueur : <t:{next_hunt:.0f}:R>",
|
154
264
|
thumbnail=emoji.url,
|
155
265
|
)
|
156
266
|
|
@@ -179,35 +289,20 @@ class HuntCog(commands.Cog):
|
|
179
289
|
|
180
290
|
# Get if hunt is valid
|
181
291
|
async with AsyncSession(self.bot.engine) as session:
|
182
|
-
|
183
|
-
select(Hunt).where(
|
184
|
-
and_(
|
185
|
-
Hunt.guild_id == guild.id,
|
186
|
-
Hunt.channel_id == channel.id,
|
187
|
-
)
|
188
|
-
)
|
189
|
-
)
|
292
|
+
hunt = await self.get_hunt(session, guild.id, channel.id)
|
190
293
|
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
# Process the winner
|
198
|
-
async with AsyncSession(self.bot.engine) as session:
|
294
|
+
# The egg was not collected
|
295
|
+
if not hunters or not hunt:
|
296
|
+
button.label = "L'œuf n'a pas été ramassé"
|
297
|
+
button.style = discord.ButtonStyle.danger
|
298
|
+
logger.info("No Hunter for %s", message_url)
|
299
|
+
else:
|
199
300
|
# Get the count of egg by user
|
200
|
-
|
201
|
-
|
202
|
-
.
|
203
|
-
|
204
|
-
Egg.guild_id == guild.id,
|
205
|
-
Egg.user_id.in_(hunter.id for hunter in hunters),
|
206
|
-
)
|
207
|
-
)
|
208
|
-
.group_by(Egg.user_id)
|
301
|
+
eggs = await self.get_egg_count_for_members(
|
302
|
+
session,
|
303
|
+
guild_id=guild.id,
|
304
|
+
user_ids=[hunter.id for hunter in hunters],
|
209
305
|
)
|
210
|
-
eggs: dict[int, int] = dict(res.all()) # type: ignore[arg-type]
|
211
306
|
logger.info("Winner draw for %s", message_url)
|
212
307
|
|
213
308
|
ranked_hunters = self.rank_players(hunters, eggs)
|
@@ -219,7 +314,6 @@ class HuntCog(commands.Cog):
|
|
219
314
|
loser = ranked_hunters[1]
|
220
315
|
|
221
316
|
if RAND.random() < self.bot.config.hunt.game:
|
222
|
-
# TODO(dashstrom): edit timer during dual
|
223
317
|
# Update button
|
224
318
|
button.label = "Duel en cours ..."
|
225
319
|
button.style = discord.ButtonStyle.gray
|
@@ -250,50 +344,56 @@ class HuntCog(commands.Cog):
|
|
250
344
|
)
|
251
345
|
await session.commit()
|
252
346
|
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
347
|
+
# Show the embed to loser
|
348
|
+
if loser:
|
349
|
+
loser_name = loser.display_name
|
350
|
+
if len(hunters) == 2: # noqa: PLR2004
|
351
|
+
text = f"{loser_name} rate un œuf"
|
352
|
+
else:
|
353
|
+
text = agree(
|
354
|
+
"{1} et {0} autre chasseur ratent un œuf",
|
355
|
+
"{1} et {0} autres chasseurs ratent un œuf",
|
356
|
+
len(hunters) - 2,
|
357
|
+
loser_name,
|
358
|
+
)
|
359
|
+
emb = embed(
|
360
|
+
title=text,
|
361
|
+
description=action.fail.text(loser),
|
362
|
+
image=action.fail.gif,
|
264
363
|
)
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
364
|
+
await channel.send(
|
365
|
+
embed=emb,
|
366
|
+
reference=message,
|
367
|
+
delete_after=300,
|
368
|
+
)
|
369
|
+
|
370
|
+
if winner:
|
371
|
+
# Send embed for the winner
|
372
|
+
winner_eggs = eggs.get(winner.id, 0) + 1
|
373
|
+
emb = embed(
|
374
|
+
title=f"{winner.display_name} récupère un œuf",
|
375
|
+
description=action.success.text(winner),
|
376
|
+
image=action.success.gif,
|
377
|
+
thumbnail=emoji.url,
|
378
|
+
egg_count=winner_eggs,
|
379
|
+
)
|
380
|
+
await channel.send(embed=emb, reference=message)
|
381
|
+
|
382
|
+
# Update button
|
383
|
+
button.label = (
|
384
|
+
f"L'œuf a été ramassé par {winner.display_name}"
|
385
|
+
)
|
386
|
+
button.style = discord.ButtonStyle.success
|
387
|
+
logger.info(
|
388
|
+
"Winner is %s (%s) with %s",
|
389
|
+
winner,
|
390
|
+
winner.id,
|
391
|
+
agree("{0} egg", "{0} eggs", winner_eggs),
|
392
|
+
)
|
393
|
+
else:
|
394
|
+
button.label = "L'œuf a été cassé"
|
395
|
+
button.style = discord.ButtonStyle.danger
|
396
|
+
logger.info("No winner %s", message_url)
|
297
397
|
|
298
398
|
# Remove emoji and edit view
|
299
399
|
button.emoji = None
|
@@ -361,19 +461,19 @@ class HuntCog(commands.Cog):
|
|
361
461
|
async def loop_hunt(self) -> None:
|
362
462
|
"""Manage the schedule of run."""
|
363
463
|
# Create a async session
|
364
|
-
async with AsyncSession(
|
365
|
-
self.bot.engine, expire_on_commit=False
|
366
|
-
) as session:
|
464
|
+
async with AsyncSession(self.bot.engine) as session:
|
367
465
|
# Find hunt with next egg available
|
368
466
|
now = time.time()
|
369
|
-
hunts = (
|
370
|
-
|
371
|
-
).all()
|
467
|
+
hunts = await self.get_hunts_after(session, now)
|
468
|
+
hunt_ids = [hunt.channel_id for hunt in hunts]
|
372
469
|
|
373
470
|
# For each hunt, set the next run and store the channel ids
|
374
471
|
if hunts:
|
375
472
|
for hunt in hunts:
|
376
|
-
|
473
|
+
delta = self.bot.config.hunt.cooldown.rand()
|
474
|
+
if self.bot.config.in_sleep_hours():
|
475
|
+
delta *= self.bot.config.sleep.divide_hunt
|
476
|
+
next_egg = now + delta
|
377
477
|
dt_next = datetime.fromtimestamp(next_egg, tz=timezone.utc)
|
378
478
|
logger.info(
|
379
479
|
"Next hunt at %s on %s",
|
@@ -382,7 +482,6 @@ class HuntCog(commands.Cog):
|
|
382
482
|
)
|
383
483
|
hunt.next_egg = next_egg
|
384
484
|
await session.commit()
|
385
|
-
hunt_ids = [hunt.channel_id for hunt in hunts]
|
386
485
|
|
387
486
|
# Call start_hunt for each hunt
|
388
487
|
if hunt_ids:
|
easterobot/hunts/luck.py
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
"""Luck module."""
|
2
|
+
|
3
|
+
import logging
|
4
|
+
from dataclasses import dataclass
|
5
|
+
|
6
|
+
from easterobot.config import RAND, MConfig
|
7
|
+
|
8
|
+
logger = logging.getLogger(__name__)
|
9
|
+
|
10
|
+
|
11
|
+
@dataclass
|
12
|
+
class HuntLuck:
|
13
|
+
egg_count: int
|
14
|
+
luck: float
|
15
|
+
sleep_hours: bool
|
16
|
+
config: MConfig
|
17
|
+
|
18
|
+
@property
|
19
|
+
def discovered(self) -> float:
|
20
|
+
"""Discovered probability."""
|
21
|
+
prob = self.config.commands.search.discovered.probability(self.luck)
|
22
|
+
if self.sleep_hours:
|
23
|
+
prob /= self.config.sleep.divide_discovered
|
24
|
+
return prob
|
25
|
+
|
26
|
+
@property
|
27
|
+
def spotted(self) -> float:
|
28
|
+
"""Spotted probability."""
|
29
|
+
prob = self.config.commands.search.spotted.probability(self.luck)
|
30
|
+
if self.sleep_hours:
|
31
|
+
prob /= self.config.sleep.divide_spotted
|
32
|
+
return prob
|
33
|
+
|
34
|
+
def sample_discovered(self) -> bool:
|
35
|
+
"""Get if player get detected."""
|
36
|
+
if self.egg_count <= self.config.commands.search.discovered.shield:
|
37
|
+
logger.info("discovered: shield with %s eggs", self.egg_count)
|
38
|
+
return True
|
39
|
+
sample = RAND.random()
|
40
|
+
logger.info(
|
41
|
+
"discovered: expect over %.2f got %.2f",
|
42
|
+
self.discovered,
|
43
|
+
sample,
|
44
|
+
)
|
45
|
+
return self.discovered > sample
|
46
|
+
|
47
|
+
def sample_spotted(self) -> bool:
|
48
|
+
"""Get if player get spotted."""
|
49
|
+
if self.egg_count <= self.config.commands.search.spotted.shield:
|
50
|
+
logger.info("spotted: shield with %s eggs", self.egg_count)
|
51
|
+
return True
|
52
|
+
sample = RAND.random()
|
53
|
+
return self.spotted < sample
|