easterobot 1.3.2__py3-none-any.whl → 1.5.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.
Files changed (81) hide show
  1. easterobot/bot.py +14 -1
  2. easterobot/casino/__init__.py +1 -0
  3. easterobot/casino/roulette.py +269 -0
  4. easterobot/commands/__init__.py +2 -0
  5. easterobot/commands/game.py +127 -118
  6. easterobot/commands/reset.py +11 -14
  7. easterobot/commands/roulette.py +34 -0
  8. easterobot/commands/top.py +73 -65
  9. easterobot/config.py +35 -8
  10. easterobot/games/{connect.py → connect4.py} +25 -28
  11. easterobot/games/game.py +126 -54
  12. easterobot/games/rock_paper_scissor.py +33 -30
  13. easterobot/games/skyjo.py +805 -0
  14. easterobot/games/tic_tac_toe.py +19 -18
  15. easterobot/hunts/hunt.py +49 -18
  16. easterobot/hunts/rank.py +24 -2
  17. easterobot/locker.py +180 -0
  18. easterobot/models.py +9 -0
  19. easterobot/resources/config.example.yml +8 -2
  20. easterobot/resources/credits.txt +2 -0
  21. easterobot/resources/emotes/placements/s1.png +0 -0
  22. easterobot/resources/emotes/placements/s10.png +0 -0
  23. easterobot/resources/emotes/placements/s11.png +0 -0
  24. easterobot/resources/emotes/placements/s12.png +0 -0
  25. easterobot/resources/emotes/placements/s2.png +0 -0
  26. easterobot/resources/emotes/placements/s3.png +0 -0
  27. easterobot/resources/emotes/placements/s4.png +0 -0
  28. easterobot/resources/emotes/placements/s5.png +0 -0
  29. easterobot/resources/emotes/placements/s6.png +0 -0
  30. easterobot/resources/emotes/placements/s7.png +0 -0
  31. easterobot/resources/emotes/placements/s8.png +0 -0
  32. easterobot/resources/emotes/placements/s9.png +0 -0
  33. easterobot/resources/emotes/placements/sA.png +0 -0
  34. easterobot/resources/emotes/placements/sB.png +0 -0
  35. easterobot/resources/emotes/placements/sC.png +0 -0
  36. easterobot/resources/emotes/placements/sD.png +0 -0
  37. easterobot/resources/emotes/placements/sE.png +0 -0
  38. easterobot/resources/emotes/placements/sF.png +0 -0
  39. easterobot/resources/emotes/placements/sG.png +0 -0
  40. easterobot/resources/emotes/placements/sH.png +0 -0
  41. easterobot/resources/emotes/placements/sI.png +0 -0
  42. easterobot/resources/emotes/placements/sJ.png +0 -0
  43. easterobot/resources/emotes/placements/sK.png +0 -0
  44. easterobot/resources/emotes/placements/sL.png +0 -0
  45. easterobot/resources/emotes/placements/sM.png +0 -0
  46. easterobot/resources/emotes/placements/sN.png +0 -0
  47. easterobot/resources/emotes/placements/sO.png +0 -0
  48. easterobot/resources/emotes/placements/sP.png +0 -0
  49. easterobot/resources/emotes/placements/sQ.png +0 -0
  50. easterobot/resources/emotes/placements/sR.png +0 -0
  51. easterobot/resources/emotes/placements/sS.png +0 -0
  52. easterobot/resources/emotes/placements/sT.png +0 -0
  53. easterobot/resources/emotes/placements/sU.png +0 -0
  54. easterobot/resources/emotes/placements/sV.png +0 -0
  55. easterobot/resources/emotes/placements/sW.png +0 -0
  56. easterobot/resources/emotes/placements/sX.png +0 -0
  57. easterobot/resources/emotes/placements/sY.png +0 -0
  58. easterobot/resources/emotes/placements/sZ.png +0 -0
  59. easterobot/resources/emotes/placements/s_.png +0 -0
  60. easterobot/resources/emotes/skyjo/skyjo_back.png +0 -0
  61. easterobot/resources/emotes/skyjo/skyjo_m1.png +0 -0
  62. easterobot/resources/emotes/skyjo/skyjo_m2.png +0 -0
  63. easterobot/resources/emotes/skyjo/skyjo_p0.png +0 -0
  64. easterobot/resources/emotes/skyjo/skyjo_p1.png +0 -0
  65. easterobot/resources/emotes/skyjo/skyjo_p10.png +0 -0
  66. easterobot/resources/emotes/skyjo/skyjo_p11.png +0 -0
  67. easterobot/resources/emotes/skyjo/skyjo_p12.png +0 -0
  68. easterobot/resources/emotes/skyjo/skyjo_p2.png +0 -0
  69. easterobot/resources/emotes/skyjo/skyjo_p3.png +0 -0
  70. easterobot/resources/emotes/skyjo/skyjo_p4.png +0 -0
  71. easterobot/resources/emotes/skyjo/skyjo_p5.png +0 -0
  72. easterobot/resources/emotes/skyjo/skyjo_p6.png +0 -0
  73. easterobot/resources/emotes/skyjo/skyjo_p7.png +0 -0
  74. easterobot/resources/emotes/skyjo/skyjo_p8.png +0 -0
  75. easterobot/resources/emotes/skyjo/skyjo_p9.png +0 -0
  76. {easterobot-1.3.2.dist-info → easterobot-1.5.1.dist-info}/METADATA +1 -1
  77. easterobot-1.5.1.dist-info/RECORD +130 -0
  78. easterobot-1.3.2.dist-info/RECORD +0 -70
  79. {easterobot-1.3.2.dist-info → easterobot-1.5.1.dist-info}/WHEEL +0 -0
  80. {easterobot-1.3.2.dist-info → easterobot-1.5.1.dist-info}/entry_points.txt +0 -0
  81. {easterobot-1.3.2.dist-info → easterobot-1.5.1.dist-info}/licenses/LICENSE +0 -0
@@ -1,7 +1,6 @@
1
1
  """Module for reset command."""
2
2
 
3
3
  import asyncio
4
- from typing import cast
5
4
 
6
5
  import discord
7
6
  from sqlalchemy import and_, delete
@@ -9,6 +8,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
9
8
 
10
9
  from easterobot.hunts.hunt import embed
11
10
  from easterobot.models import Cooldown, Egg, Hunt
11
+ from easterobot.utils import in_seconds
12
12
 
13
13
  from .base import Context, Interaction, controlled_command, egg_command_group
14
14
 
@@ -95,21 +95,18 @@ async def reset_command(ctx: Context) -> None:
95
95
 
96
96
  cancel.callback = cancel_callback # type: ignore[assignment]
97
97
  confirm.callback = confirm_callback # type: ignore[assignment]
98
- message = cast(
99
- discord.WebhookMessage,
100
- await ctx.followup.send(
101
- embed=embed(
102
- title="Demande de réinitialisation",
103
- description=(
104
- "L'ensemble des salons, œufs "
105
- "et temps d'attentes vont être réinitialisatiés."
106
- ),
107
- # TODO(dashstrom): add timer
108
- footer="Vous avez 30 secondes pour confirmer",
98
+ message = await ctx.followup.send(
99
+ embed=embed(
100
+ title="Demande de réinitialisation",
101
+ description=(
102
+ "L'ensemble des salons, œufs "
103
+ "et temps d'attentes vont être réinitialisatiés."
104
+ f"\n\n-# Vous devez confirmer {in_seconds(30)}"
109
105
  ),
110
- ephemeral=True,
111
- view=view,
112
106
  ),
107
+ ephemeral=True,
108
+ view=view,
109
+ wait=True,
113
110
  )
114
111
  await asyncio.sleep(30.0)
115
112
  if not done:
@@ -0,0 +1,34 @@
1
+ """Command basket."""
2
+
3
+ import discord
4
+
5
+ from easterobot.casino.roulette import RouletteManager
6
+ from easterobot.commands.base import (
7
+ Context,
8
+ controlled_command,
9
+ egg_command_group,
10
+ )
11
+
12
+
13
+ @egg_command_group.command(
14
+ name="roulette",
15
+ description="Lancer la roulette",
16
+ )
17
+ @controlled_command(cooldown=True, manage_channels=True)
18
+ async def roulette_command(
19
+ ctx: Context,
20
+ ) -> None:
21
+ """Show current user basket."""
22
+ # Delay the response
23
+ if not isinstance(ctx.channel, discord.TextChannel):
24
+ await ctx.response.send_message(
25
+ "Salon invalide !",
26
+ ephemeral=True,
27
+ )
28
+ return
29
+ await ctx.response.send_message(
30
+ "Lancement de la roulette !",
31
+ ephemeral=True,
32
+ )
33
+ roulette = RouletteManager(ctx.client)
34
+ await roulette.run(ctx.channel)
@@ -1,55 +1,91 @@
1
1
  """Command top."""
2
2
 
3
3
  import logging
4
- from math import floor
5
- from typing import Optional
4
+ from typing import TYPE_CHECKING
6
5
 
7
6
  import discord
8
- from sqlalchemy import distinct, func, select
9
7
  from sqlalchemy.ext.asyncio import AsyncSession
10
8
 
11
9
  from easterobot.hunts.hunt import embed
12
10
  from easterobot.hunts.rank import Ranking
13
- from easterobot.models import Egg
14
11
 
15
- from .base import Context, Interaction, controlled_command, egg_command_group
12
+ from .base import Context, controlled_command, egg_command_group
13
+
14
+ if TYPE_CHECKING:
15
+ from easterobot.bot import Easterobot
16
16
 
17
17
  PAGE_SIZE = 10
18
18
  logger = logging.getLogger(__name__)
19
19
 
20
20
 
21
- async def embed_rank(
22
- ctx: Context,
23
- page: int,
24
- colour: Optional[discord.Colour] = None,
25
- ) -> tuple[discord.Embed, bool]:
26
- """Embed for rank."""
27
- async with AsyncSession(ctx.client.engine) as session:
28
- ranking = await Ranking.from_guild(session, ctx.guild_id)
29
- hunters = ranking.page(page, limit=PAGE_SIZE)
30
- morsels: list[str] = []
21
+ class PaginationRanking(discord.ui.View):
22
+ embed: discord.Embed
23
+
24
+ def __init__(
25
+ self,
26
+ *,
27
+ ranking: Ranking,
28
+ page: int = 0,
29
+ limit: int = PAGE_SIZE,
30
+ timeout: int = 180,
31
+ ) -> None:
32
+ """Instantiate PaginationRanking."""
33
+ super().__init__(timeout=timeout)
34
+ self._page = 0
35
+ self._limit = limit
36
+ self._ranking = ranking
37
+ self.page = page
38
+
39
+ @property
40
+ def page(self) -> int:
41
+ """Current page."""
42
+ return self._page
43
+
44
+ @page.setter
45
+ def page(self, n: int) -> None:
46
+ self._page = min(max(n, 0), self._ranking.count_page(PAGE_SIZE) - 1)
47
+ self._update()
48
+
49
+ def _update(self) -> None:
50
+ count_page = self._ranking.count_page(PAGE_SIZE)
51
+ self.previous.disabled = self._page <= 0
52
+ self.next.disabled = self._page >= count_page - 1
53
+ hunters = self._ranking.page(self._page, limit=PAGE_SIZE)
31
54
  if hunters:
32
- morsels.extend(hunter.record for hunter in hunters)
55
+ text = "\n".join(hunter.record for hunter in hunters)
33
56
  else:
34
- morsels.append("\n:spider_web: Personne n'a d'œuf")
35
- total = await session.scalar(
36
- select(func.count(distinct(Egg.user_id)).label("count")).where(
37
- Egg.guild_id == ctx.guild_id
38
- )
57
+ text = "\n:spider_web: Personne n'a d'œuf"
58
+ emb = embed(
59
+ title="Chasse aux œufs",
60
+ description=text,
61
+ footer=(f"Page {self._page + 1}/{count_page or 1}"),
39
62
  )
40
- if total is None:
41
- total = 0
42
- logger.warning("No total egg !")
43
- total = floor(total / PAGE_SIZE)
44
- text = "\n".join(morsels)
45
- emb = embed(
46
- title="Chasse aux œufs",
47
- description=text,
48
- footer=f"Page {page + 1}/{total + 1}",
63
+ if hasattr(self, "embed"):
64
+ emb.colour = self.embed.colour
65
+ self.embed = emb
66
+
67
+ @discord.ui.button(
68
+ label="<",
69
+ style=discord.ButtonStyle.gray,
49
70
  )
50
- if colour is not None:
51
- emb.colour = colour
52
- return emb, page >= total
71
+ async def previous(
72
+ self,
73
+ interaction: discord.Interaction["Easterobot"],
74
+ button: discord.ui.Button["PaginationRanking"], # noqa: ARG002
75
+ ) -> None:
76
+ """Get previous page."""
77
+ self.page -= 1
78
+ await interaction.response.edit_message(view=self, embed=self.embed)
79
+
80
+ @discord.ui.button(label=">", style=discord.ButtonStyle.gray)
81
+ async def next(
82
+ self,
83
+ interaction: discord.Interaction["Easterobot"],
84
+ button: discord.ui.Button["PaginationRanking"], # noqa: ARG002
85
+ ) -> None:
86
+ """Get next page."""
87
+ self.page += 1
88
+ await interaction.response.edit_message(view=self, embed=self.embed)
53
89
 
54
90
 
55
91
  @egg_command_group.command(
@@ -60,35 +96,7 @@ async def top_command(ctx: Context) -> None:
60
96
  """Top command."""
61
97
  await ctx.response.defer(ephemeral=True)
62
98
 
63
- view = discord.ui.View(timeout=None)
64
- previous_page: discord.ui.Button[discord.ui.View] = discord.ui.Button(
65
- label="<", style=discord.ButtonStyle.gray, disabled=True
66
- )
67
- view.add_item(previous_page)
68
- next_page: discord.ui.Button[discord.ui.View] = discord.ui.Button(
69
- label=">", style=discord.ButtonStyle.gray
70
- )
71
- view.add_item(next_page)
72
- page = 0
73
-
74
- async def edit(interaction: Interaction) -> None:
75
- previous_page.disabled = page <= 0
76
- emb, next_page.disabled = await embed_rank(
77
- ctx, page, base_embed.colour
78
- )
79
- await interaction.response.edit_message(view=view, embed=emb)
80
-
81
- async def previous_callback(interaction: Interaction) -> None:
82
- nonlocal page
83
- page = max(page - 1, 0)
84
- await edit(interaction)
85
-
86
- async def next_callback(interaction: Interaction) -> None:
87
- nonlocal page
88
- page += 1
89
- await edit(interaction)
90
-
91
- previous_page.callback = previous_callback # type: ignore[assignment]
92
- next_page.callback = next_callback # type: ignore[assignment]
93
- base_embed, next_page.disabled = await embed_rank(ctx, page)
94
- await ctx.followup.send(embed=base_embed, ephemeral=True, view=view)
99
+ async with AsyncSession(ctx.client.engine) as session:
100
+ ranking = await Ranking.from_guild(session, ctx.guild_id)
101
+ view = PaginationRanking(ranking=ranking, timeout=180)
102
+ await ctx.followup.send(embed=view.embed, ephemeral=True, view=view)
easterobot/config.py CHANGED
@@ -124,6 +124,21 @@ class ConjugableText(Serializable[str]):
124
124
  return text.replace("{user}", f"<@{member.id}>")
125
125
 
126
126
 
127
+ class CasinoEvent(msgspec.Struct):
128
+ duration: float
129
+
130
+
131
+ class MCasino(msgspec.Struct):
132
+ probability: float
133
+ roulette: CasinoEvent
134
+
135
+ def sample_event(self) -> Optional[CasinoEvent]:
136
+ """Get a random event."""
137
+ if self.probability < RAND.random():
138
+ return None
139
+ return self.roulette
140
+
141
+
127
142
  class RandomItem(
128
143
  Serializable[list[T]], # Stored form
129
144
  ):
@@ -258,6 +273,7 @@ class MCommands(msgspec.Struct, forbid_unknown_fields=True):
258
273
  help: MCommand
259
274
  edit: MCommand
260
275
  connect4: MCommand
276
+ skyjo: MCommand
261
277
  info: MCommand
262
278
  tictactoe: MCommand
263
279
  rockpaperscissor: MCommand
@@ -282,6 +298,7 @@ class MConfig(msgspec.Struct, dict=True):
282
298
  database: str
283
299
  group: str
284
300
  hunt: MHunt
301
+ casino: MCasino
285
302
  conjugation: Conjugation
286
303
  failed: RandomConjugableText
287
304
  hidden: RandomConjugableText
@@ -314,16 +331,27 @@ class MConfig(msgspec.Struct, dict=True):
314
331
  "%(data)s", "/" + self.working_directory.as_posix()
315
332
  )
316
333
 
334
+ def is_sleep_hours(self, hour: time) -> bool:
335
+ """Get if bot is currently in sleep mode.
336
+
337
+ Examples:
338
+ >>> config.is_sleep_hours(time(hour=0, minute=59))
339
+ False
340
+ >>> config.is_sleep_hours(time(hour=2))
341
+ True
342
+ >>> config.is_sleep_hours(time(hour=1))
343
+ True
344
+ """
345
+ if self.sleep.start < self.sleep.end:
346
+ return self.sleep.start <= hour < self.sleep.end
347
+ if self.sleep.start > self.sleep.end:
348
+ return self.sleep.end > hour or self.sleep.start <= hour
349
+ return False
350
+
317
351
  def in_sleep_hours(self) -> bool:
318
352
  """Get if bot is currently in sleep mode."""
319
353
  hour = datetime.now(tz=timezone.utc).time()
320
- if self.sleep.start < self.sleep.end:
321
- if self.sleep.start < hour < self.sleep.end:
322
- return True
323
- elif self.sleep.start > self.sleep.end: # noqa: SIM102
324
- if not self.sleep.start < hour < self.sleep.end:
325
- return True
326
- return False
354
+ return self.is_sleep_hours(hour)
327
355
 
328
356
  def verified_token(self) -> str:
329
357
  """Get the safe token."""
@@ -409,7 +437,6 @@ class MConfig(msgspec.Struct, dict=True):
409
437
  disable_existing_loggers=False,
410
438
  defaults=defaults,
411
439
  )
412
- self.__logging_flag = True
413
440
 
414
441
  def __str__(self) -> str:
415
442
  """Represent the Configuration."""
@@ -1,11 +1,12 @@
1
1
  """Connect4 and Connect3."""
2
2
 
3
- from functools import partial
3
+ import asyncio
4
4
  from typing import Optional
5
5
 
6
6
  import discord
7
7
  from typing_extensions import override
8
8
 
9
+ from easterobot.bot import Easterobot
9
10
  from easterobot.games.game import Game, Player
10
11
  from easterobot.utils import in_seconds
11
12
 
@@ -22,36 +23,38 @@ EMOJIS_MAPPER = {
22
23
  "🔟": 9,
23
24
  }
24
25
  EMOJIS = tuple(EMOJIS_MAPPER)
26
+ ROWS = 6
27
+ COLS = 7
28
+ WIN_COUNT = 4
25
29
 
26
30
 
27
- class Connect(Game):
28
- def __init__( # noqa: PLR0913
31
+ class Connect4(Game):
32
+ def __init__(
29
33
  self,
30
- player1: discord.Member,
31
- player2: discord.Member,
34
+ bot: Easterobot,
32
35
  message: discord.Message,
33
- rows: int,
34
- cols: int,
35
- win_count: int,
36
+ *members: discord.Member,
37
+ rows: int = ROWS,
38
+ cols: int = COLS,
39
+ win_count: int = WIN_COUNT,
36
40
  ) -> None:
37
41
  """Instantiate Connect4."""
38
42
  self.grid: list[list[Optional[Player]]] = [
39
43
  [None] * rows for _ in range(cols)
40
44
  ]
41
45
  self.timeout = False
42
- self.player1 = Player(player1, 1)
43
- self.player2 = Player(player2, 2)
44
46
  self.rows = rows
45
47
  self.cols = cols
46
48
  self.win_count = win_count
47
49
  self.turn = 0
48
- super().__init__(message)
50
+ super().__init__(bot, message, *members)
49
51
 
50
52
  async def on_start(self) -> None:
51
53
  """Run."""
52
54
  await self.update()
53
55
  await self.start_timer(61)
54
56
  for emoji in EMOJIS[: self.cols]:
57
+ await asyncio.sleep(0.1)
55
58
  await self.message.add_reaction(emoji)
56
59
 
57
60
  async def update(self) -> None:
@@ -64,12 +67,12 @@ class Connect(Game):
64
67
  player: Optional[Player] = self.current
65
68
  elif self.winner:
66
69
  forfait = "par forfait " if self.timeout else ""
67
- footer = f"\n## Gagnant {forfait}{self.winner.mention} 🎉"
70
+ footer = f"\n## Gagnant {forfait}{self.winner.member.mention} 🎉"
68
71
  player = self.current
69
72
  else:
70
73
  footer = (
71
- f"\n## Égalité entre {self.player1.member.mention} "
72
- f"et {self.player2.member.mention} 🤝"
74
+ f"\n## Égalité entre {self.players[0].member.mention} "
75
+ f"et {self.players[1].member.mention} 🤝"
73
76
  )
74
77
  player = None
75
78
  content = label
@@ -93,10 +96,7 @@ class Connect(Game):
93
96
  )
94
97
  self.message = await self.message.edit(
95
98
  embed=embed,
96
- content=(
97
- f"-# {self.player1.member.mention} "
98
- f"{self.player2.member.mention}"
99
- ),
99
+ content=f"-# {' '.join(p.member.mention for p in self.players)}",
100
100
  view=None,
101
101
  )
102
102
 
@@ -116,15 +116,15 @@ class Connect(Game):
116
116
  @property
117
117
  def current(self) -> Player:
118
118
  """Get the current member playing."""
119
- return [self.player1, self.player2][self.turn % 2]
119
+ return self.players[self.turn % 2]
120
120
 
121
121
  def piece(self, member: Optional[Player]) -> str:
122
122
  """Get the current member playing."""
123
123
  if member is None:
124
124
  return "⚪"
125
- if member == self.player1:
125
+ if member == self.players[0]:
126
126
  return "🔴"
127
- if member == self.player2:
127
+ if member == self.players[1]:
128
128
  return "🟡"
129
129
  error_message = f"Invalid member: {member!r}"
130
130
  raise ValueError(error_message)
@@ -133,9 +133,9 @@ class Connect(Game):
133
133
  """Get the current player playing."""
134
134
  if player is None:
135
135
  return discord.Colour.from_str("#d4d5d6") # Grey
136
- if player == self.player1:
136
+ if player == self.players[0]:
137
137
  return discord.Colour.from_str("#ca2a3e") # Red
138
- if player == self.player2:
138
+ if player == self.players[1]:
139
139
  return discord.Colour.from_str("#e9bb51") # Yellow
140
140
  error_message = f"Invalid player: {player!r}"
141
141
  raise ValueError(error_message)
@@ -160,7 +160,7 @@ class Connect(Game):
160
160
  else:
161
161
  return # Can't be placed
162
162
  if winner:
163
- await self.set_winner(player.member)
163
+ await self.set_winner(winner)
164
164
  elif all( # Draw case
165
165
  self.grid[col][-1] is not None for col in range(self.cols)
166
166
  ):
@@ -174,7 +174,7 @@ class Connect(Game):
174
174
  async def on_timeout(self) -> None:
175
175
  self.turn += 1
176
176
  self.timeout = True
177
- await self.set_winner(self.current.member)
177
+ await self.set_winner(self.current)
178
178
  await self.update()
179
179
 
180
180
  def _is_winner(self, col: int, row: int, player: Player) -> bool:
@@ -202,6 +202,3 @@ class Connect(Game):
202
202
  c += dx
203
203
  r += dy
204
204
  return count
205
-
206
-
207
- Connect4 = partial(Connect, rows=6, cols=7, win_count=4)