easterobot 1.0.0__py3-none-any.whl → 1.1.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/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 +34 -13
- easterobot/commands/basket.py +10 -12
- easterobot/commands/edit.py +4 -4
- easterobot/commands/enable.py +5 -1
- easterobot/commands/game.py +187 -0
- easterobot/commands/help.py +1 -1
- easterobot/commands/reset.py +1 -1
- easterobot/commands/search.py +4 -4
- 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.1.dist-info}/METADATA +11 -5
- easterobot-1.1.1.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.1.dist-info}/WHEEL +0 -0
- {easterobot-1.0.0.dist-info → easterobot-1.1.1.dist-info}/entry_points.txt +0 -0
- {easterobot-1.0.0.dist-info → easterobot-1.1.1.dist-info}/licenses/LICENSE +0 -0
easterobot/hunts/hunt.py
ADDED
@@ -0,0 +1,428 @@
|
|
1
|
+
"""Start a run."""
|
2
|
+
|
3
|
+
import asyncio
|
4
|
+
import logging
|
5
|
+
import time
|
6
|
+
from collections.abc import Awaitable
|
7
|
+
from datetime import datetime, timezone
|
8
|
+
from typing import (
|
9
|
+
Any,
|
10
|
+
Callable,
|
11
|
+
Optional,
|
12
|
+
)
|
13
|
+
|
14
|
+
import discord
|
15
|
+
from discord.ext import commands
|
16
|
+
from sqlalchemy import and_, func, select, update
|
17
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
18
|
+
|
19
|
+
from easterobot.bot import Easterobot
|
20
|
+
from easterobot.config import (
|
21
|
+
RAND,
|
22
|
+
agree,
|
23
|
+
)
|
24
|
+
from easterobot.models import Egg, Hunt
|
25
|
+
|
26
|
+
logger = logging.getLogger(__name__)
|
27
|
+
DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
|
28
|
+
|
29
|
+
|
30
|
+
class HuntCog(commands.Cog):
|
31
|
+
def __init__(self, bot: Easterobot) -> None:
|
32
|
+
"""Instantiate HuntCog."""
|
33
|
+
self.bot = bot
|
34
|
+
|
35
|
+
@commands.Cog.listener()
|
36
|
+
async def on_ready(self) -> None:
|
37
|
+
"""Handle ready event, can be trigger many time if disconnected."""
|
38
|
+
# Unlock all eggs
|
39
|
+
logger.info("Unlock all previous eggs")
|
40
|
+
async with AsyncSession(self.bot.engine) as session:
|
41
|
+
await session.execute(
|
42
|
+
update(Egg).where(Egg.lock).values(lock=False)
|
43
|
+
)
|
44
|
+
await session.commit()
|
45
|
+
|
46
|
+
# Start hunt
|
47
|
+
logger.info("Start hunt handler")
|
48
|
+
pending_hunts: set[asyncio.Task[Any]] = set()
|
49
|
+
while True:
|
50
|
+
if pending_hunts:
|
51
|
+
try:
|
52
|
+
_, pending_hunts = await asyncio.wait(
|
53
|
+
pending_hunts, timeout=1
|
54
|
+
)
|
55
|
+
except Exception as err: # noqa: BLE001
|
56
|
+
logger.critical("Unattended exception", exc_info=err)
|
57
|
+
await asyncio.sleep(5)
|
58
|
+
pending_hunts.add(asyncio.create_task(self.loop_hunt()))
|
59
|
+
|
60
|
+
async def start_hunt( # noqa: C901, PLR0912, PLR0915
|
61
|
+
self,
|
62
|
+
hunt_id: int,
|
63
|
+
description: str,
|
64
|
+
*,
|
65
|
+
member_id: Optional[int] = None,
|
66
|
+
send_method: Optional[
|
67
|
+
Callable[..., Awaitable[discord.Message]]
|
68
|
+
] = None,
|
69
|
+
) -> None:
|
70
|
+
"""Start an hunt in a channel."""
|
71
|
+
# Get the hunt channel of resolve it
|
72
|
+
channel = await self.bot.resolve_channel(hunt_id)
|
73
|
+
if not channel:
|
74
|
+
return
|
75
|
+
guild = channel.guild
|
76
|
+
|
77
|
+
# Get from config
|
78
|
+
action = self.bot.config.action.rand()
|
79
|
+
emoji = self.bot.egg_emotes.rand()
|
80
|
+
|
81
|
+
# Label and hunters
|
82
|
+
hunters: list[discord.Member] = []
|
83
|
+
label = action.text
|
84
|
+
if member_id is not None:
|
85
|
+
member = channel.guild.get_member(member_id)
|
86
|
+
if member:
|
87
|
+
hunters.append(member)
|
88
|
+
label += " (1)"
|
89
|
+
|
90
|
+
# Start hunt
|
91
|
+
logger.info("Start hunt in %s", channel.jump_url)
|
92
|
+
timeout = self.bot.config.hunt.timeout + 1
|
93
|
+
view = discord.ui.View(timeout=timeout)
|
94
|
+
button: discord.ui.Button[Any] = discord.ui.Button(
|
95
|
+
label=label,
|
96
|
+
style=discord.ButtonStyle.primary,
|
97
|
+
emoji=emoji,
|
98
|
+
)
|
99
|
+
view.add_item(button)
|
100
|
+
lock_update = asyncio.Lock()
|
101
|
+
|
102
|
+
async def button_callback(
|
103
|
+
interaction: discord.Interaction[Any],
|
104
|
+
) -> None:
|
105
|
+
# Respond later
|
106
|
+
await interaction.response.defer()
|
107
|
+
message = interaction.message
|
108
|
+
user = interaction.user
|
109
|
+
if (
|
110
|
+
message is None # Message must be loaded
|
111
|
+
or not isinstance(user, discord.Member) # Must be a member
|
112
|
+
):
|
113
|
+
logger.warning("Invalid callback for %s", guild)
|
114
|
+
return
|
115
|
+
# Check if user doesn't already claim the egg
|
116
|
+
for hunter in hunters:
|
117
|
+
if hunter.id == user.id:
|
118
|
+
logger.info(
|
119
|
+
"Already hunt by %s (%s) on %s",
|
120
|
+
user,
|
121
|
+
user.id,
|
122
|
+
message.jump_url,
|
123
|
+
)
|
124
|
+
return
|
125
|
+
|
126
|
+
# Add the user to the current users
|
127
|
+
hunters.append(user)
|
128
|
+
|
129
|
+
# Show information about the hunter
|
130
|
+
logger.info(
|
131
|
+
"Hunt (%d) by %s (%s) on %s",
|
132
|
+
len(hunters),
|
133
|
+
user,
|
134
|
+
user.id,
|
135
|
+
message.jump_url,
|
136
|
+
)
|
137
|
+
|
138
|
+
# Update button counter
|
139
|
+
async with lock_update:
|
140
|
+
button.label = action.text + f" ({len(hunters)})"
|
141
|
+
await message.edit(view=view)
|
142
|
+
|
143
|
+
# Set the button callback
|
144
|
+
button.callback = button_callback # type: ignore[method-assign]
|
145
|
+
|
146
|
+
# Set next hunt
|
147
|
+
next_hunt = time.time() + timeout
|
148
|
+
|
149
|
+
# Create and embed
|
150
|
+
emb = embed(
|
151
|
+
title="Un œuf a été découvert !",
|
152
|
+
description=description
|
153
|
+
+ f"\n\nTirage du vinqueur : <t:{next_hunt:.0f}:R>",
|
154
|
+
thumbnail=emoji.url,
|
155
|
+
)
|
156
|
+
|
157
|
+
# Send the embed in the hunt channel
|
158
|
+
if send_method is None:
|
159
|
+
message = await channel.send(embed=emb, view=view)
|
160
|
+
else:
|
161
|
+
message = await send_method(embed=emb, view=view)
|
162
|
+
|
163
|
+
# TODO(dashstrom): channel is wrong due to the send message !
|
164
|
+
# TODO(dashstrom): Why wait if timeout ???
|
165
|
+
# Wait the end of the hunt
|
166
|
+
message_url = f"{channel.jump_url}/{message.id}"
|
167
|
+
async with channel.typing():
|
168
|
+
try:
|
169
|
+
await asyncio.wait_for(
|
170
|
+
view.wait(), timeout=self.bot.config.hunt.timeout
|
171
|
+
)
|
172
|
+
except asyncio.TimeoutError:
|
173
|
+
logger.info("End hunt for %s", message_url)
|
174
|
+
|
175
|
+
# Disable button and view after hunt
|
176
|
+
button.disabled = True
|
177
|
+
view.stop()
|
178
|
+
await message.edit(view=view) # Send the stop info
|
179
|
+
|
180
|
+
# Get if hunt is valid
|
181
|
+
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
|
+
)
|
190
|
+
|
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:
|
199
|
+
# 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)
|
209
|
+
)
|
210
|
+
eggs: dict[int, int] = dict(res.all()) # type: ignore[arg-type]
|
211
|
+
logger.info("Winner draw for %s", message_url)
|
212
|
+
|
213
|
+
ranked_hunters = self.rank_players(hunters, eggs)
|
214
|
+
if len(ranked_hunters) == 1:
|
215
|
+
winner = ranked_hunters[0]
|
216
|
+
loser = None
|
217
|
+
else:
|
218
|
+
winner = ranked_hunters[0]
|
219
|
+
loser = ranked_hunters[1]
|
220
|
+
|
221
|
+
if RAND.random() < self.bot.config.hunt.game:
|
222
|
+
# TODO(dashstrom): edit timer during dual
|
223
|
+
# Update button
|
224
|
+
button.label = "Duel en cours ..."
|
225
|
+
button.style = discord.ButtonStyle.gray
|
226
|
+
|
227
|
+
# Remove emoji and edit view
|
228
|
+
await message.edit(view=view)
|
229
|
+
duel_winner = await self.bot.game.dual(
|
230
|
+
channel=channel,
|
231
|
+
reference=message,
|
232
|
+
user1=winner,
|
233
|
+
user2=loser,
|
234
|
+
)
|
235
|
+
if duel_winner is None:
|
236
|
+
winner = None
|
237
|
+
loser = None
|
238
|
+
elif duel_winner == loser:
|
239
|
+
winner, loser = loser, winner
|
240
|
+
|
241
|
+
if winner:
|
242
|
+
# Add the egg to the member
|
243
|
+
session.add(
|
244
|
+
Egg(
|
245
|
+
channel_id=channel.id,
|
246
|
+
guild_id=channel.guild.id,
|
247
|
+
user_id=winner.id,
|
248
|
+
emoji_id=emoji.id,
|
249
|
+
)
|
250
|
+
)
|
251
|
+
await session.commit()
|
252
|
+
|
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,
|
264
|
+
)
|
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)
|
297
|
+
|
298
|
+
# Remove emoji and edit view
|
299
|
+
button.emoji = None
|
300
|
+
await message.edit(view=view)
|
301
|
+
|
302
|
+
def rank_players(
|
303
|
+
self, hunters: list[discord.Member], eggs: dict[int, int]
|
304
|
+
) -> list[discord.Member]:
|
305
|
+
"""Get a random working of player.
|
306
|
+
|
307
|
+
Use their egg and the order of hunt join.
|
308
|
+
"""
|
309
|
+
# If only one hunter, give the egg to him
|
310
|
+
if len(hunters) <= 1:
|
311
|
+
return hunters
|
312
|
+
lh = len(hunters)
|
313
|
+
|
314
|
+
# Get egg difference
|
315
|
+
min_eggs = min(eggs.values(), default=0)
|
316
|
+
max_eggs = max(eggs.values(), default=0)
|
317
|
+
diff_eggs = max_eggs - min_eggs
|
318
|
+
|
319
|
+
# Normalize weights
|
320
|
+
w_egg = self.bot.config.hunt.weights.egg
|
321
|
+
w_speed = self.bot.config.hunt.weights.speed
|
322
|
+
w_base = self.bot.config.hunt.weights.base
|
323
|
+
w = w_egg + w_speed + w_base
|
324
|
+
w_egg /= w
|
325
|
+
w_speed /= w
|
326
|
+
w_base /= w
|
327
|
+
|
328
|
+
# Weights by hunters
|
329
|
+
weights = []
|
330
|
+
|
331
|
+
# Compute chances of each hunters
|
332
|
+
for i, h in enumerate(hunters):
|
333
|
+
p_base = 1
|
334
|
+
if diff_eggs != 0:
|
335
|
+
egg = eggs.get(h.id, 0) - min_eggs
|
336
|
+
p_egg = 1 - egg / diff_eggs
|
337
|
+
else:
|
338
|
+
p_egg = 1.0
|
339
|
+
|
340
|
+
p_speed = 1 - i / lh
|
341
|
+
w = p_base * w_base + p_speed * w_speed + p_egg * w_egg
|
342
|
+
weights.append(w)
|
343
|
+
|
344
|
+
# Normalize final probabilities
|
345
|
+
r = sum(weights)
|
346
|
+
weights = [p / r for p in weights]
|
347
|
+
for h, w in zip(hunters, weights):
|
348
|
+
logger.info("%.2f%% - %s (%s)", w * 100, h, h.id)
|
349
|
+
|
350
|
+
rankings = []
|
351
|
+
choices_hunters = list(hunters)
|
352
|
+
while choices_hunters:
|
353
|
+
# Get the winner
|
354
|
+
(hunter,) = RAND.choices(choices_hunters, weights)
|
355
|
+
index = choices_hunters.index(hunter)
|
356
|
+
del choices_hunters[index]
|
357
|
+
del weights[index]
|
358
|
+
rankings.append(hunter)
|
359
|
+
return rankings
|
360
|
+
|
361
|
+
async def loop_hunt(self) -> None:
|
362
|
+
"""Manage the schedule of run."""
|
363
|
+
# Create a async session
|
364
|
+
async with AsyncSession(
|
365
|
+
self.bot.engine, expire_on_commit=False
|
366
|
+
) as session:
|
367
|
+
# Find hunt with next egg available
|
368
|
+
now = time.time()
|
369
|
+
hunts = (
|
370
|
+
await session.scalars(select(Hunt).where(Hunt.next_egg <= now))
|
371
|
+
).all()
|
372
|
+
|
373
|
+
# For each hunt, set the next run and store the channel ids
|
374
|
+
if hunts:
|
375
|
+
for hunt in hunts:
|
376
|
+
next_egg = now + self.bot.config.hunt.cooldown.rand()
|
377
|
+
dt_next = datetime.fromtimestamp(next_egg, tz=timezone.utc)
|
378
|
+
logger.info(
|
379
|
+
"Next hunt at %s on %s",
|
380
|
+
hunt.jump_url,
|
381
|
+
dt_next.strftime(DATE_FORMAT),
|
382
|
+
)
|
383
|
+
hunt.next_egg = next_egg
|
384
|
+
await session.commit()
|
385
|
+
hunt_ids = [hunt.channel_id for hunt in hunts]
|
386
|
+
|
387
|
+
# Call start_hunt for each hunt
|
388
|
+
if hunt_ids:
|
389
|
+
try:
|
390
|
+
await asyncio.gather(
|
391
|
+
*[
|
392
|
+
self.start_hunt(hunt_id, self.bot.config.appear.rand())
|
393
|
+
for hunt_id in hunt_ids
|
394
|
+
]
|
395
|
+
)
|
396
|
+
except Exception as err:
|
397
|
+
logger.exception(
|
398
|
+
"An error occurred during start hunt", exc_info=err
|
399
|
+
)
|
400
|
+
|
401
|
+
|
402
|
+
def embed( # noqa: PLR0913
|
403
|
+
*,
|
404
|
+
title: str,
|
405
|
+
description: Optional[str] = None,
|
406
|
+
image: Optional[str] = None,
|
407
|
+
thumbnail: Optional[str] = None,
|
408
|
+
egg_count: Optional[int] = None,
|
409
|
+
footer: Optional[str] = None,
|
410
|
+
) -> discord.Embed:
|
411
|
+
"""Create an embed with default format."""
|
412
|
+
new_embed = discord.Embed(
|
413
|
+
title=title,
|
414
|
+
description=description,
|
415
|
+
colour=RAND.randint(0, 1 << 24),
|
416
|
+
type="gifv" if image else "rich",
|
417
|
+
)
|
418
|
+
if image is not None:
|
419
|
+
new_embed.set_image(url=image)
|
420
|
+
if thumbnail is not None:
|
421
|
+
new_embed.set_thumbnail(url=thumbnail)
|
422
|
+
if egg_count is not None:
|
423
|
+
footer = (footer + " - ") if footer else ""
|
424
|
+
footer += "Cela lui fait un total de "
|
425
|
+
footer += agree("{0} œuf", "{0} œufs", egg_count)
|
426
|
+
if footer:
|
427
|
+
new_embed.set_footer(text=footer)
|
428
|
+
return new_embed
|
easterobot/hunts/rank.py
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
"""Start a run."""
|
2
|
+
|
3
|
+
from dataclasses import dataclass
|
4
|
+
|
5
|
+
from sqlalchemy import func, select
|
6
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
7
|
+
|
8
|
+
from easterobot.config import agree
|
9
|
+
from easterobot.models import Egg
|
10
|
+
|
11
|
+
RANK_MEDAL = {1: "🥇", 2: "🥈", 3: "🥉"}
|
12
|
+
|
13
|
+
|
14
|
+
@dataclass
|
15
|
+
class Hunter:
|
16
|
+
member_id: int
|
17
|
+
rank: int
|
18
|
+
eggs: int
|
19
|
+
|
20
|
+
@property
|
21
|
+
def badge(self) -> str:
|
22
|
+
"""Get the ranking badge."""
|
23
|
+
return RANK_MEDAL.get(self.rank, f"`#{self.rank}`")
|
24
|
+
|
25
|
+
@property
|
26
|
+
def record(self) -> str:
|
27
|
+
"""Get the records of eggs."""
|
28
|
+
return (
|
29
|
+
f"{self.badge} <@{self.member_id}>\n"
|
30
|
+
f"\u2004\u2004\u2004\u2004\u2004"
|
31
|
+
f"➥ {agree('{0} œuf', '{0} œufs', self.eggs)}"
|
32
|
+
)
|
33
|
+
|
34
|
+
|
35
|
+
class Ranking:
|
36
|
+
def __init__(self, hunters: list[Hunter]) -> None:
|
37
|
+
"""Initialise Ranking."""
|
38
|
+
self.hunters = hunters
|
39
|
+
|
40
|
+
def all(self) -> list[Hunter]:
|
41
|
+
"""Get all hunter."""
|
42
|
+
return self.hunters
|
43
|
+
|
44
|
+
def over(self, limit: int) -> list[Hunter]:
|
45
|
+
"""Get all hunter over limit."""
|
46
|
+
return [h for h in self.hunters if h.eggs >= limit]
|
47
|
+
|
48
|
+
def page(self, n: int, *, limit: int) -> list[Hunter]:
|
49
|
+
"""Get a hunters by page."""
|
50
|
+
if n < 0 or limit < 0:
|
51
|
+
return []
|
52
|
+
return self.hunters[limit * n : limit * (n + 1)]
|
53
|
+
|
54
|
+
def get(self, member_id: int) -> Hunter:
|
55
|
+
"""Get a hunter."""
|
56
|
+
for hunter in self.hunters:
|
57
|
+
if hunter.member_id == member_id:
|
58
|
+
return hunter
|
59
|
+
return Hunter(member_id, min(len(self.hunters), 1), 0)
|
60
|
+
|
61
|
+
@staticmethod
|
62
|
+
async def from_guild(
|
63
|
+
session: AsyncSession,
|
64
|
+
guild_id: int,
|
65
|
+
) -> "Ranking":
|
66
|
+
"""Get ranks by page."""
|
67
|
+
query = (
|
68
|
+
select(
|
69
|
+
Egg.user_id,
|
70
|
+
func.rank().over(order_by=func.count().desc()).label("row"),
|
71
|
+
func.count().label("count"),
|
72
|
+
)
|
73
|
+
.where(Egg.guild_id == guild_id)
|
74
|
+
.group_by(Egg.user_id)
|
75
|
+
.order_by(func.count().desc())
|
76
|
+
)
|
77
|
+
res = await session.execute(query)
|
78
|
+
hunters = [
|
79
|
+
Hunter(member_id, rank, egg_count)
|
80
|
+
for member_id, rank, egg_count in res.all()
|
81
|
+
]
|
82
|
+
return Ranking(hunters)
|
easterobot/models.py
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
"""Module for models."""
|
2
2
|
|
3
|
-
from sqlalchemy import BigInteger, Integer
|
3
|
+
from sqlalchemy import BigInteger, Boolean, Integer
|
4
4
|
from sqlalchemy.orm import (
|
5
5
|
DeclarativeBase,
|
6
6
|
Mapped,
|
@@ -29,6 +29,7 @@ class Egg(Base):
|
|
29
29
|
BigInteger, nullable=False, index=True
|
30
30
|
)
|
31
31
|
emoji_id: Mapped[int] = mapped_column(BigInteger, nullable=True)
|
32
|
+
lock: Mapped[bool] = mapped_column(Boolean, default=False)
|
32
33
|
|
33
34
|
@property
|
34
35
|
def jump_url(self) -> str:
|
@@ -0,0 +1,87 @@
|
|
1
|
+
# A generic, single database configuration.
|
2
|
+
|
3
|
+
[alembic]
|
4
|
+
# path to migration scripts
|
5
|
+
# Use forward slashes (/) also on windows to provide an os agnostic path
|
6
|
+
# Set using the project files
|
7
|
+
# script_location = alembic
|
8
|
+
|
9
|
+
|
10
|
+
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
|
11
|
+
# Uncomment the line below if you want the files to be prepended with date and time
|
12
|
+
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
|
13
|
+
# for all available tokens
|
14
|
+
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
|
15
|
+
|
16
|
+
# sys.path path, will be prepended to sys.path if present.
|
17
|
+
# defaults to the current working directory.
|
18
|
+
prepend_sys_path = .
|
19
|
+
|
20
|
+
# timezone to use when rendering the date within the migration file
|
21
|
+
# as well as the filename.
|
22
|
+
# If specified, requires the python>=3.9 or backports.zoneinfo library and tzdata library.
|
23
|
+
# Any required deps can installed by adding `alembic[tz]` to the pip requirements
|
24
|
+
# string value is passed to ZoneInfo()
|
25
|
+
# leave blank for localtime
|
26
|
+
# timezone =
|
27
|
+
|
28
|
+
# max length of characters to apply to the "slug" field
|
29
|
+
# truncate_slug_length = 40
|
30
|
+
|
31
|
+
# set to 'true' to run the environment during
|
32
|
+
# the 'revision' command, regardless of autogenerate
|
33
|
+
# revision_environment = false
|
34
|
+
|
35
|
+
# set to 'true' to allow .pyc and .pyo files without
|
36
|
+
# a source .py file to be detected as revisions in the
|
37
|
+
# versions/ directory
|
38
|
+
# sourceless = false
|
39
|
+
|
40
|
+
# version location specification; This defaults
|
41
|
+
# to alembic/versions. When using multiple version
|
42
|
+
# directories, initial revisions must be specified with --version-path.
|
43
|
+
# The path separator used here should be the separator specified by "version_path_separator" below.
|
44
|
+
# version_locations = %(here)s/bar:%(here)s/bat:alembic/versions
|
45
|
+
|
46
|
+
# version path separator; As mentioned above, this is the character used to split
|
47
|
+
# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
|
48
|
+
# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
|
49
|
+
# Valid values for version_path_separator are:
|
50
|
+
#
|
51
|
+
# version_path_separator = :
|
52
|
+
# version_path_separator = ;
|
53
|
+
# version_path_separator = space
|
54
|
+
# version_path_separator = newline
|
55
|
+
#
|
56
|
+
# Use os.pathsep. Default configuration used for new projects.
|
57
|
+
version_path_separator = os
|
58
|
+
|
59
|
+
# set to 'true' to search source files recursively
|
60
|
+
# in each "version_locations" directory
|
61
|
+
# new in Alembic version 1.10
|
62
|
+
# recursive_version_locations = false
|
63
|
+
|
64
|
+
# the output encoding used when revision files
|
65
|
+
# are written from script.py.mako
|
66
|
+
# output_encoding = utf-8
|
67
|
+
|
68
|
+
# Argument is set from config.yml
|
69
|
+
# sqlalchemy.url = sqlite+aiosqlite://easterobot.db
|
70
|
+
|
71
|
+
|
72
|
+
[post_write_hooks]
|
73
|
+
# post_write_hooks defines scripts or Python functions that are run
|
74
|
+
# on newly generated revision scripts. See the documentation for further
|
75
|
+
# detail and examples
|
76
|
+
|
77
|
+
# format using "black" - use the console_scripts runner, against the "black" entrypoint
|
78
|
+
# hooks = black
|
79
|
+
# black.type = console_scripts
|
80
|
+
# black.entrypoint = black
|
81
|
+
# black.options = -l 79 REVISION_SCRIPT_FILENAME
|
82
|
+
|
83
|
+
# lint with attempts to fix using "ruff" - use the exec runner, execute a binary
|
84
|
+
# hooks = ruff
|
85
|
+
# ruff.type = exec
|
86
|
+
# ruff.executable = %(here)s/.venv/bin/ruff
|
87
|
+
# ruff.options = check --fix REVISION_SCRIPT_FILENAME
|
@@ -11,14 +11,17 @@ database: sqlite+aiosqlite://%(data)s/easterobot.db
|
|
11
11
|
group: egg
|
12
12
|
hunt:
|
13
13
|
timeout: 300.0
|
14
|
+
game: 0.6
|
14
15
|
cooldown:
|
15
16
|
min: 1200.0
|
16
17
|
max: 3000.0
|
17
18
|
weights:
|
19
|
+
base: 0.3
|
18
20
|
egg: 0.4
|
19
|
-
speed: 0.
|
21
|
+
speed: 0.3
|
20
22
|
commands:
|
21
23
|
search:
|
24
|
+
cooldown: 600.0
|
22
25
|
discovered:
|
23
26
|
shield: 5
|
24
27
|
min: 0.5
|
@@ -27,9 +30,14 @@ commands:
|
|
27
30
|
shield: 10
|
28
31
|
min: 0.4
|
29
32
|
max: 0.7
|
30
|
-
cooldown: 600.0
|
31
33
|
top:
|
32
34
|
cooldown: 10.0
|
35
|
+
connect4:
|
36
|
+
cooldown: 120.0
|
37
|
+
rockpaperscissor:
|
38
|
+
cooldown: 120.0
|
39
|
+
tictactoe:
|
40
|
+
cooldown: 120.0
|
33
41
|
basket:
|
34
42
|
cooldown: 10.0
|
35
43
|
reset:
|
easterobot/resources/credits.txt
CHANGED
@@ -1 +1,5 @@
|
|
1
|
-
https://openclipart.org/detail/169059/easter-eggs-by-viscious-speedViscious-SpeedColorfulEasterEggsclip
|
1
|
+
eggs/ https://openclipart.org/detail/169059/easter-eggs-by-viscious-speedViscious-SpeedColorfulEasterEggsclip
|
2
|
+
icons/arrow.png https://www.flaticon.com/free-icon/right-arrow_10570067
|
3
|
+
icons/end.png https://www.flaticon.com/free-icon/end_5553850
|
4
|
+
icons/versus.png https://www.flaticon.com/free-icon/versus_7960356
|
5
|
+
icons/wait.png https://www.flaticon.com/free-icon/hourglass_3874060
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|