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/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 HuntCog(commands.Cog):
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 session.execute(
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 vinqueur : <t:{next_hunt:.0f}:R>",
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
- has_hunt = await session.scalar(
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
- # The egg was not collected
192
- if not hunters or not has_hunt:
193
- button.label = "L'œuf n'a pas été ramassé"
194
- button.style = discord.ButtonStyle.danger
195
- logger.info("No Hunter for %s", message_url)
196
- else:
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
- res = await session.execute(
201
- select(Egg.user_id, func.count().label("count"))
202
- .where(
203
- and_(
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
- # Show the embed to loser
254
- if loser:
255
- loser_name = loser.display_name
256
- if len(hunters) == 2: # noqa: PLR2004
257
- text = f"{loser_name} rate un œuf"
258
- else:
259
- text = agree(
260
- "{1} et {0} autre chasseur ratent un œuf",
261
- "{1} et {0} autres chasseurs ratent un œuf",
262
- len(hunters) - 2,
263
- loser_name,
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
- emb = embed(
266
- title=text,
267
- description=action.fail.text(loser),
268
- image=action.fail.gif,
269
- )
270
- await channel.send(embed=emb, reference=message)
271
-
272
- if winner:
273
- # Send embed for the winner
274
- winner_eggs = eggs.get(winner.id, 0) + 1
275
- emb = embed(
276
- title=f"{winner.display_name} récupère un œuf",
277
- description=action.success.text(winner),
278
- image=action.success.gif,
279
- thumbnail=emoji.url,
280
- egg_count=winner_eggs,
281
- )
282
- await channel.send(embed=emb, reference=message)
283
-
284
- # Update button
285
- button.label = f"L'œuf a été ramassé par {winner.display_name}"
286
- button.style = discord.ButtonStyle.success
287
- logger.info(
288
- "Winner is %s (%s) with %s",
289
- winner,
290
- winner.id,
291
- agree("{0} egg", "{0} eggs", winner_eggs),
292
- )
293
- else:
294
- button.label = "L'œuf a été cassé"
295
- button.style = discord.ButtonStyle.danger
296
- logger.info("No winner %s", message_url)
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
- await session.scalars(select(Hunt).where(Hunt.next_egg <= now))
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
- next_egg = now + self.bot.config.hunt.cooldown.rand()
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:
@@ -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
easterobot/query.py ADDED
@@ -0,0 +1,11 @@
1
+ """Base class for cog query."""
2
+
3
+ from easterobot.config import (
4
+ MConfig,
5
+ )
6
+
7
+
8
+ class QueryManager:
9
+ def __init__(self, config: MConfig):
10
+ """Instantiate QueryQueryManager."""
11
+ self.config = config