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.
- {easterobot-1.3.2 → easterobot-1.5.1}/.gitignore +1 -0
- {easterobot-1.3.2 → easterobot-1.5.1}/.vscode/settings.json +1 -0
- {easterobot-1.3.2 → easterobot-1.5.1}/PKG-INFO +1 -1
- {easterobot-1.3.2 → easterobot-1.5.1}/conftest.py +4 -3
- {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/bot.py +14 -1
- easterobot-1.5.1/easterobot/casino/__init__.py +1 -0
- easterobot-1.5.1/easterobot/casino/roulette.py +269 -0
- {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/commands/__init__.py +2 -0
- easterobot-1.5.1/easterobot/commands/game.py +200 -0
- {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/commands/reset.py +11 -14
- easterobot-1.5.1/easterobot/commands/roulette.py +34 -0
- easterobot-1.5.1/easterobot/commands/top.py +102 -0
- {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/config.py +35 -8
- easterobot-1.3.2/easterobot/games/connect.py → easterobot-1.5.1/easterobot/games/connect4.py +25 -28
- {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/games/game.py +126 -54
- {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/games/rock_paper_scissor.py +33 -30
- easterobot-1.5.1/easterobot/games/skyjo.py +805 -0
- {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/games/tic_tac_toe.py +19 -18
- {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/hunts/hunt.py +49 -18
- {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/hunts/rank.py +24 -2
- easterobot-1.5.1/easterobot/locker.py +180 -0
- {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/models.py +9 -0
- {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/resources/config.example.yml +8 -2
- {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/resources/credits.txt +2 -0
- easterobot-1.5.1/easterobot/resources/emotes/placements/s1.png +0 -0
- easterobot-1.5.1/easterobot/resources/emotes/placements/s10.png +0 -0
- easterobot-1.5.1/easterobot/resources/emotes/placements/s11.png +0 -0
- easterobot-1.5.1/easterobot/resources/emotes/placements/s12.png +0 -0
- easterobot-1.5.1/easterobot/resources/emotes/placements/s2.png +0 -0
- easterobot-1.5.1/easterobot/resources/emotes/placements/s3.png +0 -0
- easterobot-1.5.1/easterobot/resources/emotes/placements/s4.png +0 -0
- easterobot-1.5.1/easterobot/resources/emotes/placements/s5.png +0 -0
- easterobot-1.5.1/easterobot/resources/emotes/placements/s6.png +0 -0
- easterobot-1.5.1/easterobot/resources/emotes/placements/s7.png +0 -0
- easterobot-1.5.1/easterobot/resources/emotes/placements/s8.png +0 -0
- easterobot-1.5.1/easterobot/resources/emotes/placements/s9.png +0 -0
- easterobot-1.5.1/easterobot/resources/emotes/placements/sA.png +0 -0
- easterobot-1.5.1/easterobot/resources/emotes/placements/sB.png +0 -0
- easterobot-1.5.1/easterobot/resources/emotes/placements/sC.png +0 -0
- easterobot-1.5.1/easterobot/resources/emotes/placements/sD.png +0 -0
- easterobot-1.5.1/easterobot/resources/emotes/placements/sE.png +0 -0
- easterobot-1.5.1/easterobot/resources/emotes/placements/sF.png +0 -0
- easterobot-1.5.1/easterobot/resources/emotes/placements/sG.png +0 -0
- easterobot-1.5.1/easterobot/resources/emotes/placements/sH.png +0 -0
- easterobot-1.5.1/easterobot/resources/emotes/placements/sI.png +0 -0
- easterobot-1.5.1/easterobot/resources/emotes/placements/sJ.png +0 -0
- easterobot-1.5.1/easterobot/resources/emotes/placements/sK.png +0 -0
- easterobot-1.5.1/easterobot/resources/emotes/placements/sL.png +0 -0
- easterobot-1.5.1/easterobot/resources/emotes/placements/sM.png +0 -0
- easterobot-1.5.1/easterobot/resources/emotes/placements/sN.png +0 -0
- easterobot-1.5.1/easterobot/resources/emotes/placements/sO.png +0 -0
- easterobot-1.5.1/easterobot/resources/emotes/placements/sP.png +0 -0
- easterobot-1.5.1/easterobot/resources/emotes/placements/sQ.png +0 -0
- easterobot-1.5.1/easterobot/resources/emotes/placements/sR.png +0 -0
- easterobot-1.5.1/easterobot/resources/emotes/placements/sS.png +0 -0
- easterobot-1.5.1/easterobot/resources/emotes/placements/sT.png +0 -0
- easterobot-1.5.1/easterobot/resources/emotes/placements/sU.png +0 -0
- easterobot-1.5.1/easterobot/resources/emotes/placements/sV.png +0 -0
- easterobot-1.5.1/easterobot/resources/emotes/placements/sW.png +0 -0
- easterobot-1.5.1/easterobot/resources/emotes/placements/sX.png +0 -0
- easterobot-1.5.1/easterobot/resources/emotes/placements/sY.png +0 -0
- easterobot-1.5.1/easterobot/resources/emotes/placements/sZ.png +0 -0
- easterobot-1.5.1/easterobot/resources/emotes/placements/s_.png +0 -0
- easterobot-1.5.1/easterobot/resources/emotes/skyjo/skyjo_back.png +0 -0
- easterobot-1.5.1/easterobot/resources/emotes/skyjo/skyjo_m1.png +0 -0
- easterobot-1.5.1/easterobot/resources/emotes/skyjo/skyjo_m2.png +0 -0
- easterobot-1.5.1/easterobot/resources/emotes/skyjo/skyjo_p0.png +0 -0
- easterobot-1.5.1/easterobot/resources/emotes/skyjo/skyjo_p1.png +0 -0
- easterobot-1.5.1/easterobot/resources/emotes/skyjo/skyjo_p10.png +0 -0
- easterobot-1.5.1/easterobot/resources/emotes/skyjo/skyjo_p11.png +0 -0
- easterobot-1.5.1/easterobot/resources/emotes/skyjo/skyjo_p12.png +0 -0
- easterobot-1.5.1/easterobot/resources/emotes/skyjo/skyjo_p2.png +0 -0
- easterobot-1.5.1/easterobot/resources/emotes/skyjo/skyjo_p3.png +0 -0
- easterobot-1.5.1/easterobot/resources/emotes/skyjo/skyjo_p4.png +0 -0
- easterobot-1.5.1/easterobot/resources/emotes/skyjo/skyjo_p5.png +0 -0
- easterobot-1.5.1/easterobot/resources/emotes/skyjo/skyjo_p6.png +0 -0
- easterobot-1.5.1/easterobot/resources/emotes/skyjo/skyjo_p7.png +0 -0
- easterobot-1.5.1/easterobot/resources/emotes/skyjo/skyjo_p8.png +0 -0
- easterobot-1.5.1/easterobot/resources/emotes/skyjo/skyjo_p9.png +0 -0
- {easterobot-1.3.2 → easterobot-1.5.1}/pyproject.toml +2 -1
- easterobot-1.5.1/tools/gen_grid_emoji.py +98 -0
- easterobot-1.5.1/tools/gg sans Bold.ttf +0 -0
- {easterobot-1.3.2 → easterobot-1.5.1}/uv.lock +91 -1
- easterobot-1.3.2/easterobot/commands/game.py +0 -191
- easterobot-1.3.2/easterobot/commands/top.py +0 -94
- {easterobot-1.3.2 → easterobot-1.5.1}/.dockerignore +0 -0
- {easterobot-1.3.2 → easterobot-1.5.1}/.editorconfig +0 -0
- {easterobot-1.3.2 → easterobot-1.5.1}/.github/actions/setup-project/action.yml +0 -0
- {easterobot-1.3.2 → easterobot-1.5.1}/.github/workflows/docs.yml +0 -0
- {easterobot-1.3.2 → easterobot-1.5.1}/.github/workflows/lint.yml +0 -0
- {easterobot-1.3.2 → easterobot-1.5.1}/.github/workflows/publish.yml +0 -0
- {easterobot-1.3.2 → easterobot-1.5.1}/.github/workflows/tests.yml +0 -0
- {easterobot-1.3.2 → easterobot-1.5.1}/.pre-commit-config.yaml +0 -0
- {easterobot-1.3.2 → easterobot-1.5.1}/.vscode/extensions.json +0 -0
- {easterobot-1.3.2 → easterobot-1.5.1}/.vscode/ltex.dictionary.en-US.txt +0 -0
- {easterobot-1.3.2 → easterobot-1.5.1}/.vscode/ltex.hiddenFalsePositives.en-US.txt +0 -0
- {easterobot-1.3.2 → easterobot-1.5.1}/Dockerfile +0 -0
- {easterobot-1.3.2 → easterobot-1.5.1}/LICENSE +0 -0
- {easterobot-1.3.2 → easterobot-1.5.1}/README.rst +0 -0
- {easterobot-1.3.2 → easterobot-1.5.1}/docker-compose.yml +0 -0
- {easterobot-1.3.2 → easterobot-1.5.1}/docs/conf.py +0 -0
- {easterobot-1.3.2 → easterobot-1.5.1}/docs/index.rst +0 -0
- {easterobot-1.3.2 → easterobot-1.5.1}/docs/references.rst +0 -0
- {easterobot-1.3.2 → easterobot-1.5.1}/docs/resources/favicon.png +0 -0
- {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/__init__.py +0 -0
- {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/__main__.py +0 -0
- {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/alembic/env.py +0 -0
- {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/alembic/script.py.mako +0 -0
- {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/alembic/versions/2f0d4305e320_init_database.py +0 -0
- {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/alembic/versions/940c3b9c702d_add_lock_on_eggs.py +0 -0
- {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/cli.py +0 -0
- {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/commands/base.py +0 -0
- {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/commands/basket.py +0 -0
- {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/commands/disable.py +0 -0
- {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/commands/edit.py +0 -0
- {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/commands/enable.py +0 -0
- {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/commands/help.py +0 -0
- {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/commands/info.py +0 -0
- {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/commands/search.py +0 -0
- {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/games/__init__.py +0 -0
- {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/hunts/__init__.py +0 -0
- {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/hunts/luck.py +0 -0
- {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/info.py +0 -0
- {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/logger.py +0 -0
- {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/py.typed +0 -0
- {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/query.py +0 -0
- {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/resources/alembic.ini +0 -0
- {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/resources/emotes/eggs/egg_01.png +0 -0
- {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/resources/emotes/eggs/egg_02.png +0 -0
- {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/resources/emotes/eggs/egg_03.png +0 -0
- {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/resources/emotes/eggs/egg_04.png +0 -0
- {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/resources/emotes/eggs/egg_05.png +0 -0
- {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/resources/emotes/eggs/egg_06.png +0 -0
- {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/resources/emotes/eggs/egg_07.png +0 -0
- {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/resources/emotes/eggs/egg_08.png +0 -0
- {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/resources/emotes/eggs/egg_09.png +0 -0
- {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/resources/emotes/eggs/egg_10.png +0 -0
- {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/resources/emotes/eggs/egg_11.png +0 -0
- {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/resources/emotes/eggs/egg_12.png +0 -0
- {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/resources/emotes/eggs/egg_13.png +0 -0
- {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/resources/emotes/eggs/egg_14.png +0 -0
- {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/resources/emotes/eggs/egg_15.png +0 -0
- {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/resources/emotes/eggs/egg_16.png +0 -0
- {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/resources/emotes/eggs/egg_17.png +0 -0
- {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/resources/emotes/eggs/egg_18.png +0 -0
- {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/resources/emotes/eggs/egg_19.png +0 -0
- {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/resources/emotes/eggs/egg_20.png +0 -0
- {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/resources/emotes/icons/arrow.png +0 -0
- {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/resources/emotes/icons/end.png +0 -0
- {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/resources/emotes/icons/versus.png +0 -0
- {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/resources/emotes/icons/wait.png +0 -0
- {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/resources/logging.conf +0 -0
- {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/resources/logo.png +0 -0
- {easterobot-1.3.2 → easterobot-1.5.1}/easterobot/utils.py +0 -0
- {easterobot-1.3.2 → easterobot-1.5.1}/entrypoint.sh +0 -0
- {easterobot-1.3.2 → easterobot-1.5.1}/tests/__init__.py +0 -0
- {easterobot-1.3.2 → easterobot-1.5.1}/tests/constants.py +0 -0
- {easterobot-1.3.2 → easterobot-1.5.1}/tests/test_cli.py +0 -0
- {easterobot-1.3.2 → easterobot-1.5.1}/tests/test_config.py +0 -0
- {easterobot-1.3.2 → easterobot-1.5.1}/tests/test_search.py +0 -0
- {easterobot-1.3.2 → easterobot-1.5.1}/tools/chatgpt.txt +0 -0
- {easterobot-1.3.2 → easterobot-1.5.1}/tools/cropping.py +0 -0
@@ -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
|
16
|
+
def _add_bot(doctest_namespace: dict[str, Any], bot: Easterobot) -> None:
|
18
17
|
"""Update doctest namespace."""
|
19
|
-
doctest_namespace["
|
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
|
-
|
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 =
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
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)
|