easterobot 1.3.2__tar.gz → 1.5.2__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 (163) hide show
  1. {easterobot-1.3.2 → easterobot-1.5.2}/.gitignore +1 -0
  2. {easterobot-1.3.2 → easterobot-1.5.2}/.vscode/settings.json +1 -0
  3. {easterobot-1.3.2 → easterobot-1.5.2}/PKG-INFO +23 -19
  4. {easterobot-1.3.2 → easterobot-1.5.2}/README.rst +22 -18
  5. {easterobot-1.3.2 → easterobot-1.5.2}/conftest.py +4 -3
  6. {easterobot-1.3.2 → easterobot-1.5.2}/easterobot/bot.py +14 -1
  7. easterobot-1.5.2/easterobot/casino/__init__.py +1 -0
  8. easterobot-1.5.2/easterobot/casino/roulette.py +269 -0
  9. {easterobot-1.3.2 → easterobot-1.5.2}/easterobot/commands/__init__.py +2 -0
  10. easterobot-1.5.2/easterobot/commands/game.py +204 -0
  11. {easterobot-1.3.2 → easterobot-1.5.2}/easterobot/commands/reset.py +11 -14
  12. easterobot-1.5.2/easterobot/commands/roulette.py +34 -0
  13. easterobot-1.5.2/easterobot/commands/top.py +102 -0
  14. {easterobot-1.3.2 → easterobot-1.5.2}/easterobot/config.py +35 -8
  15. easterobot-1.3.2/easterobot/games/connect.py → easterobot-1.5.2/easterobot/games/connect4.py +25 -28
  16. {easterobot-1.3.2 → easterobot-1.5.2}/easterobot/games/game.py +126 -54
  17. {easterobot-1.3.2 → easterobot-1.5.2}/easterobot/games/rock_paper_scissor.py +33 -30
  18. easterobot-1.5.2/easterobot/games/skyjo.py +805 -0
  19. {easterobot-1.3.2 → easterobot-1.5.2}/easterobot/games/tic_tac_toe.py +19 -18
  20. {easterobot-1.3.2 → easterobot-1.5.2}/easterobot/hunts/hunt.py +49 -18
  21. {easterobot-1.3.2 → easterobot-1.5.2}/easterobot/hunts/rank.py +24 -2
  22. easterobot-1.5.2/easterobot/info.py +28 -0
  23. easterobot-1.5.2/easterobot/locker.py +180 -0
  24. {easterobot-1.3.2 → easterobot-1.5.2}/easterobot/models.py +9 -0
  25. {easterobot-1.3.2 → easterobot-1.5.2}/easterobot/resources/config.example.yml +8 -2
  26. {easterobot-1.3.2 → easterobot-1.5.2}/easterobot/resources/credits.txt +2 -0
  27. easterobot-1.5.2/easterobot/resources/emotes/placements/s1.png +0 -0
  28. easterobot-1.5.2/easterobot/resources/emotes/placements/s10.png +0 -0
  29. easterobot-1.5.2/easterobot/resources/emotes/placements/s11.png +0 -0
  30. easterobot-1.5.2/easterobot/resources/emotes/placements/s12.png +0 -0
  31. easterobot-1.5.2/easterobot/resources/emotes/placements/s2.png +0 -0
  32. easterobot-1.5.2/easterobot/resources/emotes/placements/s3.png +0 -0
  33. easterobot-1.5.2/easterobot/resources/emotes/placements/s4.png +0 -0
  34. easterobot-1.5.2/easterobot/resources/emotes/placements/s5.png +0 -0
  35. easterobot-1.5.2/easterobot/resources/emotes/placements/s6.png +0 -0
  36. easterobot-1.5.2/easterobot/resources/emotes/placements/s7.png +0 -0
  37. easterobot-1.5.2/easterobot/resources/emotes/placements/s8.png +0 -0
  38. easterobot-1.5.2/easterobot/resources/emotes/placements/s9.png +0 -0
  39. easterobot-1.5.2/easterobot/resources/emotes/placements/sA.png +0 -0
  40. easterobot-1.5.2/easterobot/resources/emotes/placements/sB.png +0 -0
  41. easterobot-1.5.2/easterobot/resources/emotes/placements/sC.png +0 -0
  42. easterobot-1.5.2/easterobot/resources/emotes/placements/sD.png +0 -0
  43. easterobot-1.5.2/easterobot/resources/emotes/placements/sE.png +0 -0
  44. easterobot-1.5.2/easterobot/resources/emotes/placements/sF.png +0 -0
  45. easterobot-1.5.2/easterobot/resources/emotes/placements/sG.png +0 -0
  46. easterobot-1.5.2/easterobot/resources/emotes/placements/sH.png +0 -0
  47. easterobot-1.5.2/easterobot/resources/emotes/placements/sI.png +0 -0
  48. easterobot-1.5.2/easterobot/resources/emotes/placements/sJ.png +0 -0
  49. easterobot-1.5.2/easterobot/resources/emotes/placements/sK.png +0 -0
  50. easterobot-1.5.2/easterobot/resources/emotes/placements/sL.png +0 -0
  51. easterobot-1.5.2/easterobot/resources/emotes/placements/sM.png +0 -0
  52. easterobot-1.5.2/easterobot/resources/emotes/placements/sN.png +0 -0
  53. easterobot-1.5.2/easterobot/resources/emotes/placements/sO.png +0 -0
  54. easterobot-1.5.2/easterobot/resources/emotes/placements/sP.png +0 -0
  55. easterobot-1.5.2/easterobot/resources/emotes/placements/sQ.png +0 -0
  56. easterobot-1.5.2/easterobot/resources/emotes/placements/sR.png +0 -0
  57. easterobot-1.5.2/easterobot/resources/emotes/placements/sS.png +0 -0
  58. easterobot-1.5.2/easterobot/resources/emotes/placements/sT.png +0 -0
  59. easterobot-1.5.2/easterobot/resources/emotes/placements/sU.png +0 -0
  60. easterobot-1.5.2/easterobot/resources/emotes/placements/sV.png +0 -0
  61. easterobot-1.5.2/easterobot/resources/emotes/placements/sW.png +0 -0
  62. easterobot-1.5.2/easterobot/resources/emotes/placements/sX.png +0 -0
  63. easterobot-1.5.2/easterobot/resources/emotes/placements/sY.png +0 -0
  64. easterobot-1.5.2/easterobot/resources/emotes/placements/sZ.png +0 -0
  65. easterobot-1.5.2/easterobot/resources/emotes/placements/s_.png +0 -0
  66. easterobot-1.5.2/easterobot/resources/emotes/skyjo/skyjo_back.png +0 -0
  67. easterobot-1.5.2/easterobot/resources/emotes/skyjo/skyjo_m1.png +0 -0
  68. easterobot-1.5.2/easterobot/resources/emotes/skyjo/skyjo_m2.png +0 -0
  69. easterobot-1.5.2/easterobot/resources/emotes/skyjo/skyjo_p0.png +0 -0
  70. easterobot-1.5.2/easterobot/resources/emotes/skyjo/skyjo_p1.png +0 -0
  71. easterobot-1.5.2/easterobot/resources/emotes/skyjo/skyjo_p10.png +0 -0
  72. easterobot-1.5.2/easterobot/resources/emotes/skyjo/skyjo_p11.png +0 -0
  73. easterobot-1.5.2/easterobot/resources/emotes/skyjo/skyjo_p12.png +0 -0
  74. easterobot-1.5.2/easterobot/resources/emotes/skyjo/skyjo_p2.png +0 -0
  75. easterobot-1.5.2/easterobot/resources/emotes/skyjo/skyjo_p3.png +0 -0
  76. easterobot-1.5.2/easterobot/resources/emotes/skyjo/skyjo_p4.png +0 -0
  77. easterobot-1.5.2/easterobot/resources/emotes/skyjo/skyjo_p5.png +0 -0
  78. easterobot-1.5.2/easterobot/resources/emotes/skyjo/skyjo_p6.png +0 -0
  79. easterobot-1.5.2/easterobot/resources/emotes/skyjo/skyjo_p7.png +0 -0
  80. easterobot-1.5.2/easterobot/resources/emotes/skyjo/skyjo_p8.png +0 -0
  81. easterobot-1.5.2/easterobot/resources/emotes/skyjo/skyjo_p9.png +0 -0
  82. {easterobot-1.3.2 → easterobot-1.5.2}/pyproject.toml +2 -1
  83. easterobot-1.5.2/tools/gen_grid_emoji.py +98 -0
  84. easterobot-1.5.2/tools/gg sans Bold.ttf +0 -0
  85. {easterobot-1.3.2 → easterobot-1.5.2}/uv.lock +91 -1
  86. easterobot-1.3.2/easterobot/commands/game.py +0 -191
  87. easterobot-1.3.2/easterobot/commands/top.py +0 -94
  88. easterobot-1.3.2/easterobot/info.py +0 -18
  89. {easterobot-1.3.2 → easterobot-1.5.2}/.dockerignore +0 -0
  90. {easterobot-1.3.2 → easterobot-1.5.2}/.editorconfig +0 -0
  91. {easterobot-1.3.2 → easterobot-1.5.2}/.github/actions/setup-project/action.yml +0 -0
  92. {easterobot-1.3.2 → easterobot-1.5.2}/.github/workflows/docs.yml +0 -0
  93. {easterobot-1.3.2 → easterobot-1.5.2}/.github/workflows/lint.yml +0 -0
  94. {easterobot-1.3.2 → easterobot-1.5.2}/.github/workflows/publish.yml +0 -0
  95. {easterobot-1.3.2 → easterobot-1.5.2}/.github/workflows/tests.yml +0 -0
  96. {easterobot-1.3.2 → easterobot-1.5.2}/.pre-commit-config.yaml +0 -0
  97. {easterobot-1.3.2 → easterobot-1.5.2}/.vscode/extensions.json +0 -0
  98. {easterobot-1.3.2 → easterobot-1.5.2}/.vscode/ltex.dictionary.en-US.txt +0 -0
  99. {easterobot-1.3.2 → easterobot-1.5.2}/.vscode/ltex.hiddenFalsePositives.en-US.txt +0 -0
  100. {easterobot-1.3.2 → easterobot-1.5.2}/Dockerfile +0 -0
  101. {easterobot-1.3.2 → easterobot-1.5.2}/LICENSE +0 -0
  102. {easterobot-1.3.2 → easterobot-1.5.2}/docker-compose.yml +0 -0
  103. {easterobot-1.3.2 → easterobot-1.5.2}/docs/conf.py +0 -0
  104. {easterobot-1.3.2 → easterobot-1.5.2}/docs/index.rst +0 -0
  105. {easterobot-1.3.2 → easterobot-1.5.2}/docs/references.rst +0 -0
  106. {easterobot-1.3.2 → easterobot-1.5.2}/docs/resources/favicon.png +0 -0
  107. {easterobot-1.3.2 → easterobot-1.5.2}/easterobot/__init__.py +0 -0
  108. {easterobot-1.3.2 → easterobot-1.5.2}/easterobot/__main__.py +0 -0
  109. {easterobot-1.3.2 → easterobot-1.5.2}/easterobot/alembic/env.py +0 -0
  110. {easterobot-1.3.2 → easterobot-1.5.2}/easterobot/alembic/script.py.mako +0 -0
  111. {easterobot-1.3.2 → easterobot-1.5.2}/easterobot/alembic/versions/2f0d4305e320_init_database.py +0 -0
  112. {easterobot-1.3.2 → easterobot-1.5.2}/easterobot/alembic/versions/940c3b9c702d_add_lock_on_eggs.py +0 -0
  113. {easterobot-1.3.2 → easterobot-1.5.2}/easterobot/cli.py +0 -0
  114. {easterobot-1.3.2 → easterobot-1.5.2}/easterobot/commands/base.py +0 -0
  115. {easterobot-1.3.2 → easterobot-1.5.2}/easterobot/commands/basket.py +0 -0
  116. {easterobot-1.3.2 → easterobot-1.5.2}/easterobot/commands/disable.py +0 -0
  117. {easterobot-1.3.2 → easterobot-1.5.2}/easterobot/commands/edit.py +0 -0
  118. {easterobot-1.3.2 → easterobot-1.5.2}/easterobot/commands/enable.py +0 -0
  119. {easterobot-1.3.2 → easterobot-1.5.2}/easterobot/commands/help.py +0 -0
  120. {easterobot-1.3.2 → easterobot-1.5.2}/easterobot/commands/info.py +0 -0
  121. {easterobot-1.3.2 → easterobot-1.5.2}/easterobot/commands/search.py +0 -0
  122. {easterobot-1.3.2 → easterobot-1.5.2}/easterobot/games/__init__.py +0 -0
  123. {easterobot-1.3.2 → easterobot-1.5.2}/easterobot/hunts/__init__.py +0 -0
  124. {easterobot-1.3.2 → easterobot-1.5.2}/easterobot/hunts/luck.py +0 -0
  125. {easterobot-1.3.2 → easterobot-1.5.2}/easterobot/logger.py +0 -0
  126. {easterobot-1.3.2 → easterobot-1.5.2}/easterobot/py.typed +0 -0
  127. {easterobot-1.3.2 → easterobot-1.5.2}/easterobot/query.py +0 -0
  128. {easterobot-1.3.2 → easterobot-1.5.2}/easterobot/resources/alembic.ini +0 -0
  129. {easterobot-1.3.2 → easterobot-1.5.2}/easterobot/resources/emotes/eggs/egg_01.png +0 -0
  130. {easterobot-1.3.2 → easterobot-1.5.2}/easterobot/resources/emotes/eggs/egg_02.png +0 -0
  131. {easterobot-1.3.2 → easterobot-1.5.2}/easterobot/resources/emotes/eggs/egg_03.png +0 -0
  132. {easterobot-1.3.2 → easterobot-1.5.2}/easterobot/resources/emotes/eggs/egg_04.png +0 -0
  133. {easterobot-1.3.2 → easterobot-1.5.2}/easterobot/resources/emotes/eggs/egg_05.png +0 -0
  134. {easterobot-1.3.2 → easterobot-1.5.2}/easterobot/resources/emotes/eggs/egg_06.png +0 -0
  135. {easterobot-1.3.2 → easterobot-1.5.2}/easterobot/resources/emotes/eggs/egg_07.png +0 -0
  136. {easterobot-1.3.2 → easterobot-1.5.2}/easterobot/resources/emotes/eggs/egg_08.png +0 -0
  137. {easterobot-1.3.2 → easterobot-1.5.2}/easterobot/resources/emotes/eggs/egg_09.png +0 -0
  138. {easterobot-1.3.2 → easterobot-1.5.2}/easterobot/resources/emotes/eggs/egg_10.png +0 -0
  139. {easterobot-1.3.2 → easterobot-1.5.2}/easterobot/resources/emotes/eggs/egg_11.png +0 -0
  140. {easterobot-1.3.2 → easterobot-1.5.2}/easterobot/resources/emotes/eggs/egg_12.png +0 -0
  141. {easterobot-1.3.2 → easterobot-1.5.2}/easterobot/resources/emotes/eggs/egg_13.png +0 -0
  142. {easterobot-1.3.2 → easterobot-1.5.2}/easterobot/resources/emotes/eggs/egg_14.png +0 -0
  143. {easterobot-1.3.2 → easterobot-1.5.2}/easterobot/resources/emotes/eggs/egg_15.png +0 -0
  144. {easterobot-1.3.2 → easterobot-1.5.2}/easterobot/resources/emotes/eggs/egg_16.png +0 -0
  145. {easterobot-1.3.2 → easterobot-1.5.2}/easterobot/resources/emotes/eggs/egg_17.png +0 -0
  146. {easterobot-1.3.2 → easterobot-1.5.2}/easterobot/resources/emotes/eggs/egg_18.png +0 -0
  147. {easterobot-1.3.2 → easterobot-1.5.2}/easterobot/resources/emotes/eggs/egg_19.png +0 -0
  148. {easterobot-1.3.2 → easterobot-1.5.2}/easterobot/resources/emotes/eggs/egg_20.png +0 -0
  149. {easterobot-1.3.2 → easterobot-1.5.2}/easterobot/resources/emotes/icons/arrow.png +0 -0
  150. {easterobot-1.3.2 → easterobot-1.5.2}/easterobot/resources/emotes/icons/end.png +0 -0
  151. {easterobot-1.3.2 → easterobot-1.5.2}/easterobot/resources/emotes/icons/versus.png +0 -0
  152. {easterobot-1.3.2 → easterobot-1.5.2}/easterobot/resources/emotes/icons/wait.png +0 -0
  153. {easterobot-1.3.2 → easterobot-1.5.2}/easterobot/resources/logging.conf +0 -0
  154. {easterobot-1.3.2 → easterobot-1.5.2}/easterobot/resources/logo.png +0 -0
  155. {easterobot-1.3.2 → easterobot-1.5.2}/easterobot/utils.py +0 -0
  156. {easterobot-1.3.2 → easterobot-1.5.2}/entrypoint.sh +0 -0
  157. {easterobot-1.3.2 → easterobot-1.5.2}/tests/__init__.py +0 -0
  158. {easterobot-1.3.2 → easterobot-1.5.2}/tests/constants.py +0 -0
  159. {easterobot-1.3.2 → easterobot-1.5.2}/tests/test_cli.py +0 -0
  160. {easterobot-1.3.2 → easterobot-1.5.2}/tests/test_config.py +0 -0
  161. {easterobot-1.3.2 → easterobot-1.5.2}/tests/test_search.py +0 -0
  162. {easterobot-1.3.2 → easterobot-1.5.2}/tools/chatgpt.txt +0 -0
  163. {easterobot-1.3.2 → easterobot-1.5.2}/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.2
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
@@ -144,24 +144,28 @@ Configuration directory
144
144
 
145
145
  .. code-block:: text
146
146
 
147
- data Root directory
148
- ├── .gitignore Avoid pushing sensitive data
149
- ├── config.yml Configuration file
150
- ├── easterobot.db Database
151
- ├── logs Logging directory
152
- │ ├── easterobot.log Latest log file
153
- │ └── easterobot.log.1 Rotating log file
154
- └── resources Resource directory
155
- ├── config.example.yml An example of config
156
- ├── credits.txt Credits of emotes
157
- ├── emotes Directory loaded as application emotes
158
- │ ├── eggs Directory for eggs
159
- │ | └── egg_01.png Emoji to use for egg
160
- └── icons Misc emotes to load
161
- └── arrow.png Emoji used in messages
162
- ├── logging.conf Logging configuration
163
- ├── alembic.ini Configure for alembic
164
- └── logo.png Logo used by the bot
147
+ data Root directory
148
+ ├── .gitignore Avoid pushing sensitive data
149
+ ├── config.yml Configuration file
150
+ ├── easterobot.db Database
151
+ ├── logs Logging directory
152
+ │ ├── easterobot.log Latest log file
153
+ │ └── easterobot.log.1 Rotating log file
154
+ └── resources Resource directory
155
+ ├── config.example.yml An example of config
156
+ ├── credits.txt Credits of emotes
157
+ ├── emotes Directory loaded as application emotes
158
+ │ ├── eggs Directory for eggs
159
+ │ | └── egg_01.png Emoji to use for egg
160
+ ├── icons Misc emotes to load
161
+ └── arrow.png Emoji used in messages
162
+ ├── placements Directory for emoji used in grid
163
+ │ │ └── s1.png Single blue emoji with one on it
164
+ └── skyjo Skyjo cards
165
+ │ └── skyjo_m1.png Card with minus -1 with deep blue
166
+ ├── logging.conf Logging configuration
167
+ ├── alembic.ini Configure for alembic
168
+ └── logo.png Logo used by the bot
165
169
 
166
170
  Development
167
171
  ###########
@@ -106,24 +106,28 @@ Configuration directory
106
106
 
107
107
  .. code-block:: text
108
108
 
109
- data Root directory
110
- ├── .gitignore Avoid pushing sensitive data
111
- ├── config.yml Configuration file
112
- ├── easterobot.db Database
113
- ├── logs Logging directory
114
- │ ├── easterobot.log Latest log file
115
- │ └── easterobot.log.1 Rotating log file
116
- └── resources Resource directory
117
- ├── config.example.yml An example of config
118
- ├── credits.txt Credits of emotes
119
- ├── emotes Directory loaded as application emotes
120
- │ ├── eggs Directory for eggs
121
- │ | └── egg_01.png Emoji to use for egg
122
- └── icons Misc emotes to load
123
- └── arrow.png Emoji used in messages
124
- ├── logging.conf Logging configuration
125
- ├── alembic.ini Configure for alembic
126
- └── logo.png Logo used by the bot
109
+ data Root directory
110
+ ├── .gitignore Avoid pushing sensitive data
111
+ ├── config.yml Configuration file
112
+ ├── easterobot.db Database
113
+ ├── logs Logging directory
114
+ │ ├── easterobot.log Latest log file
115
+ │ └── easterobot.log.1 Rotating log file
116
+ └── resources Resource directory
117
+ ├── config.example.yml An example of config
118
+ ├── credits.txt Credits of emotes
119
+ ├── emotes Directory loaded as application emotes
120
+ │ ├── eggs Directory for eggs
121
+ │ | └── egg_01.png Emoji to use for egg
122
+ ├── icons Misc emotes to load
123
+ └── arrow.png Emoji used in messages
124
+ ├── placements Directory for emoji used in grid
125
+ │ │ └── s1.png Single blue emoji with one on it
126
+ └── skyjo Skyjo cards
127
+ │ └── skyjo_m1.png Card with minus -1 with deep blue
128
+ ├── logging.conf Logging configuration
129
+ ├── alembic.ini Configure for alembic
130
+ └── logo.png Logo used by the bot
127
131
 
128
132
  Development
129
133
  ###########
@@ -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,204 @@
1
+ """Module for disable hunt."""
2
+
3
+ import asyncio
4
+ from contextlib import suppress
5
+ from typing import Optional
6
+
7
+ import discord
8
+ from discord import app_commands
9
+ from sqlalchemy.ext.asyncio import AsyncSession
10
+
11
+ from easterobot.config import RAND
12
+ from easterobot.games.connect4 import Connect4
13
+ from easterobot.games.game import Game
14
+ from easterobot.games.rock_paper_scissor import RockPaperScissor
15
+ from easterobot.games.skyjo import Skyjo
16
+ from easterobot.games.tic_tac_toe import TicTacToe
17
+ from easterobot.hunts.rank import Ranking
18
+ from easterobot.locker import EggLocker, EggLockerError
19
+
20
+ from .base import Context, controlled_command, egg_command_group
21
+
22
+
23
+ async def random_members(
24
+ ctx: Context,
25
+ bet: int,
26
+ ) -> list[discord.Member]:
27
+ """Random members."""
28
+ # If no member choose a random play in the guild with enough egg
29
+ if bet == 0:
30
+ members = [
31
+ m for m in ctx.guild.members if m.id != ctx.user.id and not m.bot
32
+ ]
33
+ else:
34
+ # TODO(dashstrom): can chose member with locked eggs
35
+ async with AsyncSession(ctx.client.engine) as session:
36
+ ranking = await Ranking.from_guild(
37
+ session,
38
+ ctx.guild_id,
39
+ unlock_only=True,
40
+ )
41
+ hunters = ranking.over(bet)
42
+ mapper_member = {m.id: m for m in ctx.guild.members}
43
+ members = [
44
+ mapper_member[h.member_id]
45
+ for h in hunters
46
+ if h.member_id != ctx.user.id and h.member_id in mapper_member
47
+ ]
48
+ RAND.shuffle(members)
49
+ return members
50
+
51
+
52
+ async def game_dual( # noqa: D103
53
+ ctx: Context,
54
+ bet: int,
55
+ cls: type[Game],
56
+ *members: discord.Member,
57
+ ) -> None:
58
+ set_members = set(members)
59
+ with suppress(KeyError):
60
+ set_members.remove(ctx.user)
61
+ min_player = cls.minimum_player()
62
+ max_player = cls.maximum_player()
63
+ if min_player > len(set_members) + 1:
64
+ await ctx.response.send_message(
65
+ f"Vous devez être au minimum {min_player} joueurs",
66
+ ephemeral=True,
67
+ )
68
+ return
69
+ if max_player < len(set_members) + 1:
70
+ await ctx.response.send_message(
71
+ f"Vous devez être au maximum {min_player} joueurs",
72
+ ephemeral=True,
73
+ )
74
+ return
75
+
76
+ # Check if user has enough eggs for ask
77
+ async with AsyncSession(
78
+ ctx.client.engine,
79
+ expire_on_commit=False,
80
+ ) as session:
81
+ locker = EggLocker(session, ctx.guild.id)
82
+ try:
83
+ await locker.pre_check(
84
+ {ctx.user: bet, **{m: bet for m in set_members}}
85
+ )
86
+ except EggLockerError as err:
87
+ await ctx.response.send_message(str(err), ephemeral=True)
88
+ return
89
+
90
+ msg = await ctx.client.game.ask_dual(ctx, set_members, bet=bet)
91
+ if msg:
92
+ # Unlock all egg at end
93
+ async with locker:
94
+ # Lock the egg of player
95
+ try:
96
+ async with locker.transaction():
97
+ all_eggs = await asyncio.gather(
98
+ locker.get(ctx.user, bet),
99
+ *[locker.get(m, bet) for m in set_members],
100
+ )
101
+ except EggLockerError as err:
102
+ await msg.reply(str(err), delete_after=30)
103
+ return
104
+
105
+ players = [ctx.user, *set_members]
106
+ RAND.shuffle(players)
107
+ game = cls(ctx.client, msg, *players)
108
+ await ctx.client.game.run(game)
109
+ winner = await game.wait_winner()
110
+ if winner:
111
+ for eggs in all_eggs:
112
+ for egg in eggs:
113
+ egg.user_id = winner.member.id
114
+
115
+ # Send change
116
+ await session.commit()
117
+
118
+
119
+ @egg_command_group.command(
120
+ name="connect4",
121
+ description="Lancer une partie de puissance 4",
122
+ )
123
+ @controlled_command(cooldown=True, channel_permissions={"send_messages": True})
124
+ async def connect4_command(
125
+ ctx: Context,
126
+ member: Optional[discord.Member] = None,
127
+ bet: app_commands.Range[int, 0] = 0,
128
+ ) -> None:
129
+ """Run a Connect4."""
130
+ members = (
131
+ (await random_members(ctx, bet))[:1] if member is None else [member]
132
+ )
133
+ await game_dual(ctx, bet, Connect4, *members)
134
+
135
+
136
+ @egg_command_group.command(
137
+ name="tictactoe",
138
+ description="Lancer une partie de morpion",
139
+ )
140
+ @controlled_command(cooldown=True, channel_permissions={"send_messages": True})
141
+ async def tictactoe_command(
142
+ ctx: Context,
143
+ member: Optional[discord.Member] = None,
144
+ bet: app_commands.Range[int, 0] = 0,
145
+ ) -> None:
146
+ """Run a tictactoe."""
147
+ members = (
148
+ (await random_members(ctx, bet))[:1] if member is None else [member]
149
+ )
150
+ await game_dual(ctx, bet, TicTacToe, *members)
151
+
152
+
153
+ @egg_command_group.command(
154
+ name="rockpaperscissor",
155
+ description="Lancer une partie de pierre papier ciseaux",
156
+ )
157
+ @controlled_command(cooldown=True, channel_permissions={"send_messages": True})
158
+ async def rockpaperscissor_command(
159
+ ctx: Context,
160
+ member: Optional[discord.Member] = None,
161
+ bet: app_commands.Range[int, 0] = 0,
162
+ ) -> None:
163
+ """Run a rockpaperscissor."""
164
+ members = (
165
+ (await random_members(ctx, bet))[:1] if member is None else [member]
166
+ )
167
+ await game_dual(ctx, bet, RockPaperScissor, *members)
168
+
169
+
170
+ @egg_command_group.command(
171
+ name="skyjo",
172
+ description="Lancer une partie de Skyjo",
173
+ )
174
+ @controlled_command(cooldown=True, channel_permissions={"send_messages": True})
175
+ async def skyjo_command( # noqa: PLR0913
176
+ ctx: Context,
177
+ member1: Optional[discord.Member] = None,
178
+ member2: Optional[discord.Member] = None,
179
+ member3: Optional[discord.Member] = None,
180
+ member4: Optional[discord.Member] = None,
181
+ member5: Optional[discord.Member] = None,
182
+ member6: Optional[discord.Member] = None,
183
+ member7: Optional[discord.Member] = None,
184
+ bet: app_commands.Range[int, 0] = 0,
185
+ ) -> None:
186
+ """Run a skyjo."""
187
+ members = [
188
+ m
189
+ for m in (
190
+ member1,
191
+ member2,
192
+ member3,
193
+ member4,
194
+ member5,
195
+ member6,
196
+ member7,
197
+ )
198
+ if m
199
+ ]
200
+ if not members:
201
+ player_count = RAND.randint(1, 8)
202
+ rand_members = await random_members(ctx, bet)
203
+ members = rand_members[:player_count]
204
+ await game_dual(ctx, bet, Skyjo, *members)