easterobot 1.3.2__tar.gz → 1.5.1__tar.gz

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 (162) hide show
  1. {easterobot-1.3.2 → easterobot-1.5.1}/.gitignore +1 -0
  2. {easterobot-1.3.2 → easterobot-1.5.1}/.vscode/settings.json +1 -0
  3. {easterobot-1.3.2 → easterobot-1.5.1}/PKG-INFO +1 -1
  4. {easterobot-1.3.2 → easterobot-1.5.1}/conftest.py +4 -3
  5. {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/bot.py +14 -1
  6. easterobot-1.5.1/easterobot/casino/__init__.py +1 -0
  7. easterobot-1.5.1/easterobot/casino/roulette.py +269 -0
  8. {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/commands/__init__.py +2 -0
  9. easterobot-1.5.1/easterobot/commands/game.py +200 -0
  10. {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/commands/reset.py +11 -14
  11. easterobot-1.5.1/easterobot/commands/roulette.py +34 -0
  12. easterobot-1.5.1/easterobot/commands/top.py +102 -0
  13. {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/config.py +35 -8
  14. easterobot-1.3.2/easterobot/games/connect.py → easterobot-1.5.1/easterobot/games/connect4.py +25 -28
  15. {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/games/game.py +126 -54
  16. {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/games/rock_paper_scissor.py +33 -30
  17. easterobot-1.5.1/easterobot/games/skyjo.py +805 -0
  18. {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/games/tic_tac_toe.py +19 -18
  19. {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/hunts/hunt.py +49 -18
  20. {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/hunts/rank.py +24 -2
  21. easterobot-1.5.1/easterobot/locker.py +180 -0
  22. {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/models.py +9 -0
  23. {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/resources/config.example.yml +8 -2
  24. {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/resources/credits.txt +2 -0
  25. easterobot-1.5.1/easterobot/resources/emotes/placements/s1.png +0 -0
  26. easterobot-1.5.1/easterobot/resources/emotes/placements/s10.png +0 -0
  27. easterobot-1.5.1/easterobot/resources/emotes/placements/s11.png +0 -0
  28. easterobot-1.5.1/easterobot/resources/emotes/placements/s12.png +0 -0
  29. easterobot-1.5.1/easterobot/resources/emotes/placements/s2.png +0 -0
  30. easterobot-1.5.1/easterobot/resources/emotes/placements/s3.png +0 -0
  31. easterobot-1.5.1/easterobot/resources/emotes/placements/s4.png +0 -0
  32. easterobot-1.5.1/easterobot/resources/emotes/placements/s5.png +0 -0
  33. easterobot-1.5.1/easterobot/resources/emotes/placements/s6.png +0 -0
  34. easterobot-1.5.1/easterobot/resources/emotes/placements/s7.png +0 -0
  35. easterobot-1.5.1/easterobot/resources/emotes/placements/s8.png +0 -0
  36. easterobot-1.5.1/easterobot/resources/emotes/placements/s9.png +0 -0
  37. easterobot-1.5.1/easterobot/resources/emotes/placements/sA.png +0 -0
  38. easterobot-1.5.1/easterobot/resources/emotes/placements/sB.png +0 -0
  39. easterobot-1.5.1/easterobot/resources/emotes/placements/sC.png +0 -0
  40. easterobot-1.5.1/easterobot/resources/emotes/placements/sD.png +0 -0
  41. easterobot-1.5.1/easterobot/resources/emotes/placements/sE.png +0 -0
  42. easterobot-1.5.1/easterobot/resources/emotes/placements/sF.png +0 -0
  43. easterobot-1.5.1/easterobot/resources/emotes/placements/sG.png +0 -0
  44. easterobot-1.5.1/easterobot/resources/emotes/placements/sH.png +0 -0
  45. easterobot-1.5.1/easterobot/resources/emotes/placements/sI.png +0 -0
  46. easterobot-1.5.1/easterobot/resources/emotes/placements/sJ.png +0 -0
  47. easterobot-1.5.1/easterobot/resources/emotes/placements/sK.png +0 -0
  48. easterobot-1.5.1/easterobot/resources/emotes/placements/sL.png +0 -0
  49. easterobot-1.5.1/easterobot/resources/emotes/placements/sM.png +0 -0
  50. easterobot-1.5.1/easterobot/resources/emotes/placements/sN.png +0 -0
  51. easterobot-1.5.1/easterobot/resources/emotes/placements/sO.png +0 -0
  52. easterobot-1.5.1/easterobot/resources/emotes/placements/sP.png +0 -0
  53. easterobot-1.5.1/easterobot/resources/emotes/placements/sQ.png +0 -0
  54. easterobot-1.5.1/easterobot/resources/emotes/placements/sR.png +0 -0
  55. easterobot-1.5.1/easterobot/resources/emotes/placements/sS.png +0 -0
  56. easterobot-1.5.1/easterobot/resources/emotes/placements/sT.png +0 -0
  57. easterobot-1.5.1/easterobot/resources/emotes/placements/sU.png +0 -0
  58. easterobot-1.5.1/easterobot/resources/emotes/placements/sV.png +0 -0
  59. easterobot-1.5.1/easterobot/resources/emotes/placements/sW.png +0 -0
  60. easterobot-1.5.1/easterobot/resources/emotes/placements/sX.png +0 -0
  61. easterobot-1.5.1/easterobot/resources/emotes/placements/sY.png +0 -0
  62. easterobot-1.5.1/easterobot/resources/emotes/placements/sZ.png +0 -0
  63. easterobot-1.5.1/easterobot/resources/emotes/placements/s_.png +0 -0
  64. easterobot-1.5.1/easterobot/resources/emotes/skyjo/skyjo_back.png +0 -0
  65. easterobot-1.5.1/easterobot/resources/emotes/skyjo/skyjo_m1.png +0 -0
  66. easterobot-1.5.1/easterobot/resources/emotes/skyjo/skyjo_m2.png +0 -0
  67. easterobot-1.5.1/easterobot/resources/emotes/skyjo/skyjo_p0.png +0 -0
  68. easterobot-1.5.1/easterobot/resources/emotes/skyjo/skyjo_p1.png +0 -0
  69. easterobot-1.5.1/easterobot/resources/emotes/skyjo/skyjo_p10.png +0 -0
  70. easterobot-1.5.1/easterobot/resources/emotes/skyjo/skyjo_p11.png +0 -0
  71. easterobot-1.5.1/easterobot/resources/emotes/skyjo/skyjo_p12.png +0 -0
  72. easterobot-1.5.1/easterobot/resources/emotes/skyjo/skyjo_p2.png +0 -0
  73. easterobot-1.5.1/easterobot/resources/emotes/skyjo/skyjo_p3.png +0 -0
  74. easterobot-1.5.1/easterobot/resources/emotes/skyjo/skyjo_p4.png +0 -0
  75. easterobot-1.5.1/easterobot/resources/emotes/skyjo/skyjo_p5.png +0 -0
  76. easterobot-1.5.1/easterobot/resources/emotes/skyjo/skyjo_p6.png +0 -0
  77. easterobot-1.5.1/easterobot/resources/emotes/skyjo/skyjo_p7.png +0 -0
  78. easterobot-1.5.1/easterobot/resources/emotes/skyjo/skyjo_p8.png +0 -0
  79. easterobot-1.5.1/easterobot/resources/emotes/skyjo/skyjo_p9.png +0 -0
  80. {easterobot-1.3.2 → easterobot-1.5.1}/pyproject.toml +2 -1
  81. easterobot-1.5.1/tools/gen_grid_emoji.py +98 -0
  82. easterobot-1.5.1/tools/gg sans Bold.ttf +0 -0
  83. {easterobot-1.3.2 → easterobot-1.5.1}/uv.lock +91 -1
  84. easterobot-1.3.2/easterobot/commands/game.py +0 -191
  85. easterobot-1.3.2/easterobot/commands/top.py +0 -94
  86. {easterobot-1.3.2 → easterobot-1.5.1}/.dockerignore +0 -0
  87. {easterobot-1.3.2 → easterobot-1.5.1}/.editorconfig +0 -0
  88. {easterobot-1.3.2 → easterobot-1.5.1}/.github/actions/setup-project/action.yml +0 -0
  89. {easterobot-1.3.2 → easterobot-1.5.1}/.github/workflows/docs.yml +0 -0
  90. {easterobot-1.3.2 → easterobot-1.5.1}/.github/workflows/lint.yml +0 -0
  91. {easterobot-1.3.2 → easterobot-1.5.1}/.github/workflows/publish.yml +0 -0
  92. {easterobot-1.3.2 → easterobot-1.5.1}/.github/workflows/tests.yml +0 -0
  93. {easterobot-1.3.2 → easterobot-1.5.1}/.pre-commit-config.yaml +0 -0
  94. {easterobot-1.3.2 → easterobot-1.5.1}/.vscode/extensions.json +0 -0
  95. {easterobot-1.3.2 → easterobot-1.5.1}/.vscode/ltex.dictionary.en-US.txt +0 -0
  96. {easterobot-1.3.2 → easterobot-1.5.1}/.vscode/ltex.hiddenFalsePositives.en-US.txt +0 -0
  97. {easterobot-1.3.2 → easterobot-1.5.1}/Dockerfile +0 -0
  98. {easterobot-1.3.2 → easterobot-1.5.1}/LICENSE +0 -0
  99. {easterobot-1.3.2 → easterobot-1.5.1}/README.rst +0 -0
  100. {easterobot-1.3.2 → easterobot-1.5.1}/docker-compose.yml +0 -0
  101. {easterobot-1.3.2 → easterobot-1.5.1}/docs/conf.py +0 -0
  102. {easterobot-1.3.2 → easterobot-1.5.1}/docs/index.rst +0 -0
  103. {easterobot-1.3.2 → easterobot-1.5.1}/docs/references.rst +0 -0
  104. {easterobot-1.3.2 → easterobot-1.5.1}/docs/resources/favicon.png +0 -0
  105. {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/__init__.py +0 -0
  106. {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/__main__.py +0 -0
  107. {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/alembic/env.py +0 -0
  108. {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/alembic/script.py.mako +0 -0
  109. {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/alembic/versions/2f0d4305e320_init_database.py +0 -0
  110. {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/alembic/versions/940c3b9c702d_add_lock_on_eggs.py +0 -0
  111. {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/cli.py +0 -0
  112. {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/commands/base.py +0 -0
  113. {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/commands/basket.py +0 -0
  114. {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/commands/disable.py +0 -0
  115. {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/commands/edit.py +0 -0
  116. {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/commands/enable.py +0 -0
  117. {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/commands/help.py +0 -0
  118. {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/commands/info.py +0 -0
  119. {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/commands/search.py +0 -0
  120. {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/games/__init__.py +0 -0
  121. {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/hunts/__init__.py +0 -0
  122. {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/hunts/luck.py +0 -0
  123. {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/info.py +0 -0
  124. {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/logger.py +0 -0
  125. {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/py.typed +0 -0
  126. {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/query.py +0 -0
  127. {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/resources/alembic.ini +0 -0
  128. {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/resources/emotes/eggs/egg_01.png +0 -0
  129. {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/resources/emotes/eggs/egg_02.png +0 -0
  130. {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/resources/emotes/eggs/egg_03.png +0 -0
  131. {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/resources/emotes/eggs/egg_04.png +0 -0
  132. {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/resources/emotes/eggs/egg_05.png +0 -0
  133. {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/resources/emotes/eggs/egg_06.png +0 -0
  134. {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/resources/emotes/eggs/egg_07.png +0 -0
  135. {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/resources/emotes/eggs/egg_08.png +0 -0
  136. {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/resources/emotes/eggs/egg_09.png +0 -0
  137. {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/resources/emotes/eggs/egg_10.png +0 -0
  138. {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/resources/emotes/eggs/egg_11.png +0 -0
  139. {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/resources/emotes/eggs/egg_12.png +0 -0
  140. {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/resources/emotes/eggs/egg_13.png +0 -0
  141. {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/resources/emotes/eggs/egg_14.png +0 -0
  142. {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/resources/emotes/eggs/egg_15.png +0 -0
  143. {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/resources/emotes/eggs/egg_16.png +0 -0
  144. {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/resources/emotes/eggs/egg_17.png +0 -0
  145. {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/resources/emotes/eggs/egg_18.png +0 -0
  146. {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/resources/emotes/eggs/egg_19.png +0 -0
  147. {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/resources/emotes/eggs/egg_20.png +0 -0
  148. {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/resources/emotes/icons/arrow.png +0 -0
  149. {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/resources/emotes/icons/end.png +0 -0
  150. {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/resources/emotes/icons/versus.png +0 -0
  151. {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/resources/emotes/icons/wait.png +0 -0
  152. {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/resources/logging.conf +0 -0
  153. {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/resources/logo.png +0 -0
  154. {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/utils.py +0 -0
  155. {easterobot-1.3.2 → easterobot-1.5.1}/entrypoint.sh +0 -0
  156. {easterobot-1.3.2 → easterobot-1.5.1}/tests/__init__.py +0 -0
  157. {easterobot-1.3.2 → easterobot-1.5.1}/tests/constants.py +0 -0
  158. {easterobot-1.3.2 → easterobot-1.5.1}/tests/test_cli.py +0 -0
  159. {easterobot-1.3.2 → easterobot-1.5.1}/tests/test_config.py +0 -0
  160. {easterobot-1.3.2 → easterobot-1.5.1}/tests/test_search.py +0 -0
  161. {easterobot-1.3.2 → easterobot-1.5.1}/tools/chatgpt.txt +0 -0
  162. {easterobot-1.3.2 → easterobot-1.5.1}/tools/cropping.py +0 -0
@@ -113,3 +113,4 @@ config.yml
113
113
  logs
114
114
  *.db
115
115
  /bot
116
+ /skyjo
@@ -88,6 +88,7 @@
88
88
  "pyarg",
89
89
  "pypi",
90
90
  "pyproject",
91
+ "Skyjo",
91
92
  "sourcelink",
92
93
  "sphinxcontrib",
93
94
  "todos",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: easterobot
3
- Version: 1.3.2
3
+ Version: 1.5.1
4
4
  Summary: Discord bot for Easter.
5
5
  Project-URL: Homepage, https://github.com/Dashstrom/easterobot
6
6
  Project-URL: Repository, https://github.com/Dashstrom/easterobot
@@ -8,15 +8,16 @@ import pytest
8
8
  import pytest_asyncio
9
9
  from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession
10
10
 
11
- from easterobot import __author__
12
11
  from easterobot.bot import Easterobot
13
12
  from easterobot.config import MConfig
14
13
 
15
14
 
16
15
  @pytest.fixture(autouse=True)
17
- def _add_author(doctest_namespace: dict[str, Any]) -> None:
16
+ def _add_bot(doctest_namespace: dict[str, Any], bot: Easterobot) -> None:
18
17
  """Update doctest namespace."""
19
- doctest_namespace["author"] = __author__
18
+ doctest_namespace["bot"] = bot
19
+ doctest_namespace["engine"] = bot.engine
20
+ doctest_namespace["config"] = bot.config
20
21
 
21
22
 
22
23
  @pytest.fixture
@@ -1,5 +1,6 @@
1
1
  """Main program."""
2
2
 
3
+ import asyncio
3
4
  import logging
4
5
  import pathlib
5
6
  import shutil
@@ -17,6 +18,7 @@ import discord.app_commands
17
18
  import discord.ext.commands
18
19
  from alembic.command import upgrade
19
20
  from sqlalchemy.ext.asyncio import create_async_engine
21
+ from typing_extensions import override
20
22
 
21
23
  if TYPE_CHECKING:
22
24
  from easterobot.games.game import GameCog
@@ -44,6 +46,7 @@ class Easterobot(discord.ext.commands.Bot):
44
46
  owner: discord.User
45
47
  game: "GameCog"
46
48
  hunt: "HuntCog"
49
+ init_finished: asyncio.Event
47
50
 
48
51
  def __init__(self, config: MConfig) -> None:
49
52
  """Initialise Easterbot."""
@@ -167,6 +170,12 @@ class Easterobot(discord.ext.commands.Bot):
167
170
  """Run the bot with the given token."""
168
171
  self.run(token=self.config.verified_token())
169
172
 
173
+ @override
174
+ async def start(self, token: str, *, reconnect: bool = True) -> None:
175
+ """Add event for starting."""
176
+ self.init_finished = asyncio.Event()
177
+ await super().start(token=token, reconnect=reconnect)
178
+
170
179
  async def on_ready(self) -> None:
171
180
  """Handle ready event, can be trigger many time if disconnected."""
172
181
  # Sync bot commands
@@ -196,6 +205,7 @@ class Easterobot(discord.ext.commands.Bot):
196
205
  self.user,
197
206
  getattr(self.user, "id", "unknown"),
198
207
  )
208
+ self.init_finished.set()
199
209
 
200
210
  async def _load_emojis(self) -> None:
201
211
  emojis = {
@@ -203,6 +213,8 @@ class Easterobot(discord.ext.commands.Bot):
203
213
  for emoji in await self.fetch_application_emojis()
204
214
  }
205
215
  emotes_path = (self.config.resources / "emotes").resolve()
216
+ # TODO(dashstrom): remove old one !
217
+ # TODO(dashstrom): cache emoji synced !
206
218
  self.app_emojis = {}
207
219
  for emote in emotes_path.glob("**/*"):
208
220
  if not emote.is_file():
@@ -221,4 +233,5 @@ class Easterobot(discord.ext.commands.Bot):
221
233
  self.app_emojis[name] = emoji
222
234
  else:
223
235
  logger.info("Load emoji %s", name)
224
- self.app_emojis[name] = emojis[name]
236
+ emoji = emojis[name]
237
+ self.app_emojis[name] = emoji
@@ -0,0 +1 @@
1
+ """Module for casino events."""
@@ -0,0 +1,269 @@
1
+ """Module to play roulette."""
2
+
3
+ import asyncio
4
+ from asyncio import sleep
5
+ from dataclasses import dataclass
6
+ from typing import TYPE_CHECKING, Union
7
+
8
+ import discord
9
+ from sqlalchemy.ext.asyncio import AsyncSession
10
+
11
+ from easterobot.bot import Easterobot
12
+ from easterobot.config import RAND, agree
13
+ from easterobot.locker import EggLocker
14
+ from easterobot.utils import in_seconds
15
+
16
+ if TYPE_CHECKING:
17
+ from easterobot.models import Egg
18
+
19
+
20
+ @dataclass(frozen=True, order=True)
21
+ class Play:
22
+ name: str
23
+ emoji: str
24
+ bet: int
25
+ payout: int
26
+ slots: frozenset[int]
27
+
28
+ @property
29
+ def label(self) -> str:
30
+ """Returns the label of the bet."""
31
+ return agree(
32
+ f"{self.bet} œuf sur {self.name}",
33
+ f"{self.bet} œufs sur {self.name}",
34
+ self.bet,
35
+ )
36
+
37
+ @property
38
+ def probability(self) -> float:
39
+ """Returns the winning probability."""
40
+ return len(self.slots) / 37
41
+
42
+ @property
43
+ def eggs(self) -> float:
44
+ """Returns the number of eggs won."""
45
+ return self.payout * self.bet
46
+
47
+
48
+ # fmt: off
49
+ plays = [
50
+ Play("noir", "⚫", 1, 2, frozenset({2, 4, 6, 8, 10, 11, 13, 15, 17, 20,
51
+ 22, 24, 26, 28, 29, 31, 33, 35})),
52
+ Play("rouge", "🔴", 1, 2, frozenset({1, 3, 5, 7, 9, 12, 14, 16, 18, 19, 21,
53
+ 23, 25, 27, 30, 32, 34, 36})),
54
+ Play("impaire", "1️⃣", 3, 2, frozenset(range(1, 37, 2))),
55
+ Play("pair", "2️⃣", 3, 2, frozenset(range(2, 37, 2))),
56
+ Play("manque", "⬅️", 5, 2, frozenset(range(1, 19))),
57
+ Play("passe", "➡️", 5, 2, frozenset(range(19, 37))),
58
+ Play("zero", "0️⃣", 1, 36, frozenset({0})),
59
+ ]
60
+ play_mapper = {p.label: p for p in plays}
61
+ # fmt: on
62
+
63
+
64
+ @dataclass
65
+ class RouletteResult:
66
+ draw: int
67
+ winners: dict[discord.Member, Play]
68
+ losers: dict[discord.Member, Play]
69
+
70
+ @property
71
+ def label(self) -> str:
72
+ """Returns the name(s) of the winning bet(s)."""
73
+ winning_plays = sorted(set(self.winners.values()))
74
+ if len(winning_plays) == 1:
75
+ return winning_plays[0].name
76
+ if winning_plays:
77
+ last = winning_plays[-1]
78
+ return (
79
+ ", ".join(p.name for p in winning_plays[:-1])
80
+ + " et "
81
+ + last.name
82
+ )
83
+ return "rien au numéro"
84
+
85
+
86
+ class Roulette:
87
+ def __init__(self, locker: EggLocker) -> None:
88
+ """Initialize an empty bet tracker."""
89
+ self.bets: dict[discord.Member, Play] = {}
90
+ self.eggs: dict[discord.Member, list[Egg]] = {}
91
+ self.locker = locker
92
+
93
+ async def bet(self, member: discord.Member, play: Play) -> None:
94
+ """Register a bet from a member."""
95
+ if member in self.eggs:
96
+ raise ValueError
97
+ async with self.locker.transaction():
98
+ eggs = await self.locker.get(member, play.bet)
99
+ self.eggs[member] = eggs
100
+ self.bets[member] = play
101
+
102
+ async def sample(self) -> "RouletteResult":
103
+ """Draw a number and determine winners/losers."""
104
+ ball = RAND.randint(0, 36)
105
+ losers = {}
106
+ winners = {}
107
+ futures = []
108
+ async with self.locker.transaction():
109
+ for member, play in self.bets.items():
110
+ eggs = self.eggs[member]
111
+ if ball in play.slots:
112
+ added_eggs = [
113
+ egg.duplicate()
114
+ for egg in eggs
115
+ for _ in range(play.payout - 1)
116
+ ]
117
+ self.locker.update(added_eggs)
118
+ winners[member] = play
119
+ else:
120
+ futures.append(self.locker.delete(eggs))
121
+ losers[member] = play
122
+ await asyncio.gather(*futures)
123
+ return RouletteResult(
124
+ draw=ball,
125
+ losers=losers,
126
+ winners=winners,
127
+ )
128
+
129
+
130
+ class BetView(discord.ui.View):
131
+ def __init__(self, embed: discord.Embed, roulette: Roulette) -> None:
132
+ """Create an interactive view for placing bets."""
133
+ super().__init__()
134
+ self.embed = embed
135
+ self.roulette = roulette
136
+ self.already_interact: set[discord.Member] = set()
137
+
138
+ def disable(self) -> None:
139
+ """Disable the selection UI."""
140
+ self.select_bet.disabled = True # type: ignore[attr-defined]
141
+ self.stop()
142
+
143
+ @discord.ui.select(
144
+ placeholder="Parier",
145
+ options=[
146
+ discord.SelectOption(
147
+ label=f"Parier {play.label}",
148
+ emoji=play.emoji,
149
+ value=play.label,
150
+ description=(
151
+ f"{play.probability:.2%} de repartir avec {play.eggs} œufs"
152
+ ),
153
+ )
154
+ for play in plays
155
+ ],
156
+ )
157
+ async def select_bet(
158
+ self,
159
+ interaction: discord.Interaction["Easterobot"],
160
+ select: discord.ui.Select["BetView"],
161
+ ) -> None:
162
+ """Handle the player's bet selection."""
163
+ user = interaction.user
164
+ if not isinstance(user, discord.Member) or interaction.message is None:
165
+ await interaction.response.defer()
166
+ return
167
+ if user in self.already_interact:
168
+ await interaction.response.send_message(
169
+ "Vous avez déjà choisi votre pari !",
170
+ ephemeral=True,
171
+ )
172
+ return
173
+ self.already_interact.add(user)
174
+ bet = play_mapper[select.values[0]]
175
+ await self.roulette.bet(user, bet)
176
+ embeds = interaction.message.embeds
177
+ assert self.embed.description is not None # noqa: S101
178
+ self.embed.description += (
179
+ f"\n> {interaction.user.mention} a parié {bet.label} {bet.emoji}"
180
+ )
181
+ await interaction.response.edit_message(embeds=[embeds[0], self.embed])
182
+
183
+
184
+ class RouletteManager:
185
+ def __init__(self, bot: Easterobot) -> None:
186
+ """Main manager for roulette game logic."""
187
+ self.bot = bot
188
+
189
+ async def run(
190
+ self,
191
+ source: Union[discord.Message, discord.TextChannel],
192
+ ) -> None:
193
+ """Run a full roulette session."""
194
+ guild = source.guild
195
+ if guild is None:
196
+ raise ValueError
197
+ async with (
198
+ AsyncSession(
199
+ self.bot.engine,
200
+ expire_on_commit=False,
201
+ ) as session,
202
+ EggLocker(session, guild.id) as locker,
203
+ ):
204
+ timeout = self.bot.config.casino.roulette.duration + 40
205
+ roulette = Roulette(locker)
206
+ embed = discord.Embed(
207
+ description=(
208
+ "# Roulette lapinique"
209
+ "\nLe Casino vous ouvre exceptionnellement ses portes. "
210
+ "Devant vous se trouve un élégant croupier lapin. "
211
+ "Il vous fixe droit dans les yeux "
212
+ "et prononce de simples mots en langue lapinique. "
213
+ "Magiquement, vous semblez comprendre : 'Faites vos jeux'."
214
+ "\n\n-# Faites attention, "
215
+ f"il annoncera sans doute la fin {in_seconds(timeout)}."
216
+ ),
217
+ color=0x00FF00,
218
+ )
219
+ text = discord.Embed(
220
+ description="# Annonces du croupier\n> Faites vos jeux",
221
+ color=0x00FF00,
222
+ )
223
+ assert text.description is not None # noqa: S101
224
+ embed.set_image(
225
+ url="https://i.pinimg.com/originals/32/37/bf/3237bf1e172a6089e0c437ffd3b28010.gif"
226
+ )
227
+ view = BetView(text, roulette)
228
+ if isinstance(source, discord.Message):
229
+ message = source
230
+ await message.edit(
231
+ embeds=[embed, text],
232
+ content="",
233
+ view=view,
234
+ )
235
+ else:
236
+ message = await source.send(
237
+ embeds=[embed, text],
238
+ view=view,
239
+ )
240
+ await sleep(timeout)
241
+ text.description += "\n> Les jeux sont faits"
242
+ await message.edit(embeds=[embed, text])
243
+ await sleep(20)
244
+ view.disable()
245
+ text.description += "\n> Rien ne va plus"
246
+ await message.edit(view=view, embeds=[embed, text])
247
+ await sleep(20)
248
+ result = await roulette.sample()
249
+ text.description += "\n> La bille s'arrête "
250
+ number = f"{result.draw:2d}".replace(" ", "\xa0")
251
+ text.description += f"sur le ||{number}||"
252
+ text.description += f"\n> Le lapin annonce ||{result.label}||"
253
+ await message.edit(view=None, embeds=[embed, text])
254
+
255
+ messages = []
256
+ for member, bet in result.winners.items():
257
+ egg_text = agree("œuf", "œufs", bet.bet)
258
+ messages.append(
259
+ f"{member.mention} repart avec {bet.eggs} {egg_text}"
260
+ )
261
+ for member, bet in result.losers.items():
262
+ egg_text = agree("œuf", "œufs", bet.bet)
263
+ messages.append(f"{member.mention} perd {bet.bet} {egg_text}")
264
+ if messages:
265
+ await sleep(5)
266
+ await message.reply( # type: ignore[call-overload]
267
+ content="\n".join(messages),
268
+ view=None,
269
+ )
@@ -14,6 +14,7 @@ from easterobot.commands.game import (
14
14
  from easterobot.commands.help import help_command
15
15
  from easterobot.commands.info import info_command
16
16
  from easterobot.commands.reset import reset_command
17
+ from easterobot.commands.roulette import roulette_command
17
18
  from easterobot.commands.search import search_command
18
19
  from easterobot.commands.top import top_command
19
20
 
@@ -28,6 +29,7 @@ __all__ = [
28
29
  "info_command",
29
30
  "reset_command",
30
31
  "rockpaperscissor_command",
32
+ "roulette_command",
31
33
  "search_command",
32
34
  "tictactoe_command",
33
35
  "top_command",
@@ -0,0 +1,200 @@
1
+ """Module for disable hunt."""
2
+
3
+ import asyncio
4
+ from typing import Optional
5
+
6
+ import discord
7
+ from discord import app_commands
8
+ from sqlalchemy.ext.asyncio import AsyncSession
9
+
10
+ from easterobot.config import RAND
11
+ from easterobot.games.connect4 import Connect4
12
+ from easterobot.games.game import Game
13
+ from easterobot.games.rock_paper_scissor import RockPaperScissor
14
+ from easterobot.games.skyjo import Skyjo
15
+ from easterobot.games.tic_tac_toe import TicTacToe
16
+ from easterobot.hunts.rank import Ranking
17
+ from easterobot.locker import EggLocker, EggLockerError
18
+
19
+ from .base import Context, controlled_command, egg_command_group
20
+
21
+
22
+ async def random_members(
23
+ ctx: Context,
24
+ bet: int,
25
+ ) -> list[discord.Member]:
26
+ """Random members."""
27
+ # If no member choose a random play in the guild with enough egg
28
+ if bet == 0:
29
+ members = [
30
+ m for m in ctx.guild.members if m.id != ctx.user.id and not m.bot
31
+ ]
32
+ else:
33
+ # TODO(dashstrom): can chose member with locked eggs
34
+ async with AsyncSession(ctx.client.engine) as session:
35
+ ranking = await Ranking.from_guild(
36
+ session,
37
+ ctx.guild_id,
38
+ unlock_only=True,
39
+ )
40
+ hunters = ranking.over(bet)
41
+ mapper_member = {m.id: m for m in ctx.guild.members}
42
+ members = [
43
+ mapper_member[h.member_id]
44
+ for h in hunters
45
+ if h.member_id != ctx.user.id and h.member_id in mapper_member
46
+ ]
47
+ RAND.shuffle(members)
48
+ return members
49
+
50
+
51
+ async def game_dual( # noqa: D103
52
+ ctx: Context,
53
+ bet: int,
54
+ cls: type[Game],
55
+ *members: discord.Member,
56
+ ) -> None:
57
+ min_player = cls.minimum_player()
58
+ max_player = cls.maximum_player()
59
+ if min_player > len(members) + 1:
60
+ await ctx.response.send_message(
61
+ f"Vous devez être au minimum {min_player} joueurs",
62
+ ephemeral=True,
63
+ )
64
+ return
65
+ if max_player < len(members) + 1:
66
+ await ctx.response.send_message(
67
+ f"Vous devez être au maximum {min_player} joueurs",
68
+ ephemeral=True,
69
+ )
70
+ return
71
+
72
+ # Check if user has enough eggs for ask
73
+ async with AsyncSession(
74
+ ctx.client.engine,
75
+ expire_on_commit=False,
76
+ ) as session:
77
+ locker = EggLocker(session, ctx.guild.id)
78
+ try:
79
+ await locker.pre_check(
80
+ {ctx.user: bet, **{m: bet for m in members}}
81
+ )
82
+ except EggLockerError as err:
83
+ await ctx.response.send_message(str(err), ephemeral=True)
84
+ return
85
+
86
+ msg = await ctx.client.game.ask_dual(ctx, members, bet=bet)
87
+ if msg:
88
+ # Unlock all egg at end
89
+ async with locker:
90
+ # Lock the egg of player
91
+ try:
92
+ async with locker.transaction():
93
+ all_eggs = await asyncio.gather(
94
+ locker.get(ctx.user, bet),
95
+ *[locker.get(m, bet) for m in members],
96
+ )
97
+ except EggLockerError as err:
98
+ await msg.reply(str(err), delete_after=30)
99
+ return
100
+
101
+ players = [ctx.user, *members]
102
+ RAND.shuffle(players)
103
+ game = cls(ctx.client, msg, *players)
104
+ await ctx.client.game.run(game)
105
+ winner = await game.wait_winner()
106
+ if winner:
107
+ for eggs in all_eggs:
108
+ for egg in eggs:
109
+ egg.user_id = winner.member.id
110
+
111
+ # Send change
112
+ await session.commit()
113
+
114
+
115
+ @egg_command_group.command(
116
+ name="connect4",
117
+ description="Lancer une partie de puissance 4",
118
+ )
119
+ @controlled_command(cooldown=True, channel_permissions={"send_messages": True})
120
+ async def connect4_command(
121
+ ctx: Context,
122
+ member: Optional[discord.Member] = None,
123
+ bet: app_commands.Range[int, 0] = 0,
124
+ ) -> None:
125
+ """Run a Connect4."""
126
+ members = (
127
+ (await random_members(ctx, bet))[:1] if member is None else [member]
128
+ )
129
+ await game_dual(ctx, bet, Connect4, *members)
130
+
131
+
132
+ @egg_command_group.command(
133
+ name="tictactoe",
134
+ description="Lancer une partie de morpion",
135
+ )
136
+ @controlled_command(cooldown=True, channel_permissions={"send_messages": True})
137
+ async def tictactoe_command(
138
+ ctx: Context,
139
+ member: Optional[discord.Member] = None,
140
+ bet: app_commands.Range[int, 0] = 0,
141
+ ) -> None:
142
+ """Run a tictactoe."""
143
+ members = (
144
+ (await random_members(ctx, bet))[:1] if member is None else [member]
145
+ )
146
+ await game_dual(ctx, bet, TicTacToe, *members)
147
+
148
+
149
+ @egg_command_group.command(
150
+ name="rockpaperscissor",
151
+ description="Lancer une partie de pierre papier ciseaux",
152
+ )
153
+ @controlled_command(cooldown=True, channel_permissions={"send_messages": True})
154
+ async def rockpaperscissor_command(
155
+ ctx: Context,
156
+ member: Optional[discord.Member] = None,
157
+ bet: app_commands.Range[int, 0] = 0,
158
+ ) -> None:
159
+ """Run a rockpaperscissor."""
160
+ members = (
161
+ (await random_members(ctx, bet))[:1] if member is None else [member]
162
+ )
163
+ await game_dual(ctx, bet, RockPaperScissor, *members)
164
+
165
+
166
+ @egg_command_group.command(
167
+ name="skyjo",
168
+ description="Lancer une partie de Skyjo",
169
+ )
170
+ @controlled_command(cooldown=True, channel_permissions={"send_messages": True})
171
+ async def skyjo_command( # noqa: PLR0913
172
+ ctx: Context,
173
+ member1: Optional[discord.Member] = None,
174
+ member2: Optional[discord.Member] = None,
175
+ member3: Optional[discord.Member] = None,
176
+ member4: Optional[discord.Member] = None,
177
+ member5: Optional[discord.Member] = None,
178
+ member6: Optional[discord.Member] = None,
179
+ member7: Optional[discord.Member] = None,
180
+ bet: app_commands.Range[int, 0] = 0,
181
+ ) -> None:
182
+ """Run a skyjo."""
183
+ members = [
184
+ m
185
+ for m in (
186
+ member1,
187
+ member2,
188
+ member3,
189
+ member4,
190
+ member5,
191
+ member6,
192
+ member7,
193
+ )
194
+ if m
195
+ ]
196
+ if not members:
197
+ player_count = RAND.randint(1, 8)
198
+ rand_members = await random_members(ctx, bet)
199
+ members = rand_members[:player_count]
200
+ await game_dual(ctx, bet, Skyjo, *members)
@@ -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)