easterobot 1.0.0__py3-none-any.whl → 1.1.0__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.
Files changed (58) hide show
  1. easterobot/alembic/env.py +91 -0
  2. easterobot/alembic/script.py.mako +28 -0
  3. easterobot/alembic/versions/2f0d4305e320_init_database.py +67 -0
  4. easterobot/alembic/versions/940c3b9c702d_add_lock_on_eggs.py +38 -0
  5. easterobot/bot.py +93 -462
  6. easterobot/cli.py +56 -17
  7. easterobot/commands/__init__.py +8 -0
  8. easterobot/commands/base.py +3 -7
  9. easterobot/commands/basket.py +10 -12
  10. easterobot/commands/edit.py +4 -4
  11. easterobot/commands/game.py +187 -0
  12. easterobot/commands/help.py +1 -1
  13. easterobot/commands/reset.py +1 -1
  14. easterobot/commands/search.py +3 -3
  15. easterobot/commands/top.py +7 -18
  16. easterobot/config.py +67 -3
  17. easterobot/games/__init__.py +14 -0
  18. easterobot/games/connect.py +206 -0
  19. easterobot/games/game.py +262 -0
  20. easterobot/games/rock_paper_scissor.py +206 -0
  21. easterobot/games/tic_tac_toe.py +168 -0
  22. easterobot/hunts/__init__.py +14 -0
  23. easterobot/hunts/hunt.py +428 -0
  24. easterobot/hunts/rank.py +82 -0
  25. easterobot/models.py +2 -1
  26. easterobot/resources/alembic.ini +87 -0
  27. easterobot/resources/config.example.yml +10 -2
  28. easterobot/resources/credits.txt +5 -1
  29. easterobot/resources/emotes/icons/arrow.png +0 -0
  30. easterobot/resources/emotes/icons/end.png +0 -0
  31. easterobot/resources/emotes/icons/versus.png +0 -0
  32. easterobot/resources/emotes/icons/wait.png +0 -0
  33. {easterobot-1.0.0.dist-info → easterobot-1.1.0.dist-info}/METADATA +11 -5
  34. easterobot-1.1.0.dist-info/RECORD +66 -0
  35. easterobot-1.0.0.dist-info/RECORD +0 -48
  36. /easterobot/resources/{eggs → emotes/eggs}/egg_01.png +0 -0
  37. /easterobot/resources/{eggs → emotes/eggs}/egg_02.png +0 -0
  38. /easterobot/resources/{eggs → emotes/eggs}/egg_03.png +0 -0
  39. /easterobot/resources/{eggs → emotes/eggs}/egg_04.png +0 -0
  40. /easterobot/resources/{eggs → emotes/eggs}/egg_05.png +0 -0
  41. /easterobot/resources/{eggs → emotes/eggs}/egg_06.png +0 -0
  42. /easterobot/resources/{eggs → emotes/eggs}/egg_07.png +0 -0
  43. /easterobot/resources/{eggs → emotes/eggs}/egg_08.png +0 -0
  44. /easterobot/resources/{eggs → emotes/eggs}/egg_09.png +0 -0
  45. /easterobot/resources/{eggs → emotes/eggs}/egg_10.png +0 -0
  46. /easterobot/resources/{eggs → emotes/eggs}/egg_11.png +0 -0
  47. /easterobot/resources/{eggs → emotes/eggs}/egg_12.png +0 -0
  48. /easterobot/resources/{eggs → emotes/eggs}/egg_13.png +0 -0
  49. /easterobot/resources/{eggs → emotes/eggs}/egg_14.png +0 -0
  50. /easterobot/resources/{eggs → emotes/eggs}/egg_15.png +0 -0
  51. /easterobot/resources/{eggs → emotes/eggs}/egg_16.png +0 -0
  52. /easterobot/resources/{eggs → emotes/eggs}/egg_17.png +0 -0
  53. /easterobot/resources/{eggs → emotes/eggs}/egg_18.png +0 -0
  54. /easterobot/resources/{eggs → emotes/eggs}/egg_19.png +0 -0
  55. /easterobot/resources/{eggs → emotes/eggs}/egg_20.png +0 -0
  56. {easterobot-1.0.0.dist-info → easterobot-1.1.0.dist-info}/WHEEL +0 -0
  57. {easterobot-1.0.0.dist-info → easterobot-1.1.0.dist-info}/entry_points.txt +0 -0
  58. {easterobot-1.0.0.dist-info → easterobot-1.1.0.dist-info}/licenses/LICENSE +0 -0
@@ -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
@@ -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.4
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:
@@ -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