easterobot 1.3.2__py3-none-any.whl → 1.5.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- easterobot/bot.py +14 -1
- easterobot/casino/__init__.py +1 -0
- easterobot/casino/roulette.py +269 -0
- easterobot/commands/__init__.py +2 -0
- easterobot/commands/game.py +131 -118
- easterobot/commands/reset.py +11 -14
- easterobot/commands/roulette.py +34 -0
- easterobot/commands/top.py +73 -65
- easterobot/config.py +35 -8
- easterobot/games/{connect.py → connect4.py} +25 -28
- easterobot/games/game.py +126 -54
- easterobot/games/rock_paper_scissor.py +33 -30
- easterobot/games/skyjo.py +805 -0
- easterobot/games/tic_tac_toe.py +19 -18
- easterobot/hunts/hunt.py +49 -18
- easterobot/hunts/rank.py +24 -2
- easterobot/info.py +17 -7
- easterobot/locker.py +180 -0
- easterobot/models.py +9 -0
- easterobot/resources/config.example.yml +8 -2
- easterobot/resources/credits.txt +2 -0
- easterobot/resources/emotes/placements/s1.png +0 -0
- easterobot/resources/emotes/placements/s10.png +0 -0
- easterobot/resources/emotes/placements/s11.png +0 -0
- easterobot/resources/emotes/placements/s12.png +0 -0
- easterobot/resources/emotes/placements/s2.png +0 -0
- easterobot/resources/emotes/placements/s3.png +0 -0
- easterobot/resources/emotes/placements/s4.png +0 -0
- easterobot/resources/emotes/placements/s5.png +0 -0
- easterobot/resources/emotes/placements/s6.png +0 -0
- easterobot/resources/emotes/placements/s7.png +0 -0
- easterobot/resources/emotes/placements/s8.png +0 -0
- easterobot/resources/emotes/placements/s9.png +0 -0
- easterobot/resources/emotes/placements/sA.png +0 -0
- easterobot/resources/emotes/placements/sB.png +0 -0
- easterobot/resources/emotes/placements/sC.png +0 -0
- easterobot/resources/emotes/placements/sD.png +0 -0
- easterobot/resources/emotes/placements/sE.png +0 -0
- easterobot/resources/emotes/placements/sF.png +0 -0
- easterobot/resources/emotes/placements/sG.png +0 -0
- easterobot/resources/emotes/placements/sH.png +0 -0
- easterobot/resources/emotes/placements/sI.png +0 -0
- easterobot/resources/emotes/placements/sJ.png +0 -0
- easterobot/resources/emotes/placements/sK.png +0 -0
- easterobot/resources/emotes/placements/sL.png +0 -0
- easterobot/resources/emotes/placements/sM.png +0 -0
- easterobot/resources/emotes/placements/sN.png +0 -0
- easterobot/resources/emotes/placements/sO.png +0 -0
- easterobot/resources/emotes/placements/sP.png +0 -0
- easterobot/resources/emotes/placements/sQ.png +0 -0
- easterobot/resources/emotes/placements/sR.png +0 -0
- easterobot/resources/emotes/placements/sS.png +0 -0
- easterobot/resources/emotes/placements/sT.png +0 -0
- easterobot/resources/emotes/placements/sU.png +0 -0
- easterobot/resources/emotes/placements/sV.png +0 -0
- easterobot/resources/emotes/placements/sW.png +0 -0
- easterobot/resources/emotes/placements/sX.png +0 -0
- easterobot/resources/emotes/placements/sY.png +0 -0
- easterobot/resources/emotes/placements/sZ.png +0 -0
- easterobot/resources/emotes/placements/s_.png +0 -0
- easterobot/resources/emotes/skyjo/skyjo_back.png +0 -0
- easterobot/resources/emotes/skyjo/skyjo_m1.png +0 -0
- easterobot/resources/emotes/skyjo/skyjo_m2.png +0 -0
- easterobot/resources/emotes/skyjo/skyjo_p0.png +0 -0
- easterobot/resources/emotes/skyjo/skyjo_p1.png +0 -0
- easterobot/resources/emotes/skyjo/skyjo_p10.png +0 -0
- easterobot/resources/emotes/skyjo/skyjo_p11.png +0 -0
- easterobot/resources/emotes/skyjo/skyjo_p12.png +0 -0
- easterobot/resources/emotes/skyjo/skyjo_p2.png +0 -0
- easterobot/resources/emotes/skyjo/skyjo_p3.png +0 -0
- easterobot/resources/emotes/skyjo/skyjo_p4.png +0 -0
- easterobot/resources/emotes/skyjo/skyjo_p5.png +0 -0
- easterobot/resources/emotes/skyjo/skyjo_p6.png +0 -0
- easterobot/resources/emotes/skyjo/skyjo_p7.png +0 -0
- easterobot/resources/emotes/skyjo/skyjo_p8.png +0 -0
- easterobot/resources/emotes/skyjo/skyjo_p9.png +0 -0
- {easterobot-1.3.2.dist-info → easterobot-1.5.2.dist-info}/METADATA +23 -19
- easterobot-1.5.2.dist-info/RECORD +130 -0
- easterobot-1.3.2.dist-info/RECORD +0 -70
- {easterobot-1.3.2.dist-info → easterobot-1.5.2.dist-info}/WHEEL +0 -0
- {easterobot-1.3.2.dist-info → easterobot-1.5.2.dist-info}/entry_points.txt +0 -0
- {easterobot-1.3.2.dist-info → easterobot-1.5.2.dist-info}/licenses/LICENSE +0 -0
easterobot/bot.py
CHANGED
@@ -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
|
+
)
|
easterobot/commands/__init__.py
CHANGED
@@ -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",
|
easterobot/commands/game.py
CHANGED
@@ -1,152 +1,119 @@
|
|
1
1
|
"""Module for disable hunt."""
|
2
2
|
|
3
3
|
import asyncio
|
4
|
-
from
|
4
|
+
from contextlib import suppress
|
5
|
+
from typing import Optional
|
5
6
|
|
6
7
|
import discord
|
7
8
|
from discord import app_commands
|
8
|
-
from sqlalchemy import and_, func, not_, select
|
9
9
|
from sqlalchemy.ext.asyncio import AsyncSession
|
10
10
|
|
11
11
|
from easterobot.config import RAND
|
12
|
-
from easterobot.games.
|
12
|
+
from easterobot.games.connect4 import Connect4
|
13
13
|
from easterobot.games.game import Game
|
14
14
|
from easterobot.games.rock_paper_scissor import RockPaperScissor
|
15
|
+
from easterobot.games.skyjo import Skyjo
|
15
16
|
from easterobot.games.tic_tac_toe import TicTacToe
|
16
17
|
from easterobot.hunts.rank import Ranking
|
17
|
-
from easterobot.
|
18
|
+
from easterobot.locker import EggLocker, EggLockerError
|
18
19
|
|
19
20
|
from .base import Context, controlled_command, egg_command_group
|
20
21
|
|
21
|
-
lock = asyncio.Lock()
|
22
|
-
|
23
|
-
|
24
|
-
async def get_unlocked_eggs(
|
25
|
-
session: AsyncSession, member: discord.Member, counter: int
|
26
|
-
) -> list[Egg]:
|
27
|
-
"""Get the count of unlocked eggs."""
|
28
|
-
return list(
|
29
|
-
(
|
30
|
-
await session.scalars(
|
31
|
-
select(Egg)
|
32
|
-
.where(
|
33
|
-
and_(
|
34
|
-
Egg.guild_id == member.guild.id,
|
35
|
-
Egg.user_id == member.id,
|
36
|
-
not_(Egg.lock),
|
37
|
-
)
|
38
|
-
)
|
39
|
-
.order_by(func.random()) # Randomize
|
40
|
-
.limit(counter)
|
41
|
-
)
|
42
|
-
).all()
|
43
|
-
)
|
44
|
-
|
45
22
|
|
46
|
-
async def
|
23
|
+
async def random_members(
|
47
24
|
ctx: Context,
|
48
|
-
member: Optional[discord.Member],
|
49
25
|
bet: int,
|
50
|
-
|
51
|
-
|
26
|
+
) -> list[discord.Member]:
|
27
|
+
"""Random members."""
|
52
28
|
# If no member choose a random play in the guild with enough egg
|
53
|
-
if
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
for h in hunters
|
65
|
-
if h.member_id != ctx.user.id and h.member_id in mapper_member
|
66
|
-
]
|
67
|
-
if members:
|
68
|
-
member = RAND.choice(members)
|
69
|
-
else:
|
70
|
-
await ctx.response.send_message(
|
71
|
-
"Aucun utilisateur trouvé !",
|
72
|
-
ephemeral=True,
|
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,
|
73
40
|
)
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
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:
|
80
70
|
await ctx.response.send_message(
|
81
|
-
"
|
71
|
+
f"Vous devez être au maximum {min_player} joueurs",
|
82
72
|
ephemeral=True,
|
83
73
|
)
|
84
74
|
return
|
85
75
|
|
86
76
|
# Check if user has enough eggs for ask
|
87
|
-
async with AsyncSession(
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
)
|
92
|
-
|
93
|
-
await
|
94
|
-
|
95
|
-
ephemeral=True,
|
96
|
-
)
|
97
|
-
return
|
98
|
-
if len(e2) < bet:
|
99
|
-
await ctx.response.send_message(
|
100
|
-
f"{member.mention} n'a pas assez d'œufs",
|
101
|
-
ephemeral=True,
|
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}}
|
102
85
|
)
|
86
|
+
except EggLockerError as err:
|
87
|
+
await ctx.response.send_message(str(err), ephemeral=True)
|
103
88
|
return
|
104
89
|
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
)
|
119
|
-
return
|
120
|
-
for e in e1:
|
121
|
-
e.lock = True
|
122
|
-
if len(e2) < bet:
|
123
|
-
await ctx.response.send_message(
|
124
|
-
f"{member.mention} n'a plus assez d'œufs",
|
125
|
-
ephemeral=True,
|
126
|
-
)
|
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)
|
127
103
|
return
|
128
|
-
for e in e2:
|
129
|
-
e.lock = True
|
130
|
-
await session.commit()
|
131
104
|
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
game = cls(ctx.user, member, msg)
|
105
|
+
players = [ctx.user, *set_members]
|
106
|
+
RAND.shuffle(players)
|
107
|
+
game = cls(ctx.client, msg, *players)
|
136
108
|
await ctx.client.game.run(game)
|
137
109
|
winner = await game.wait_winner()
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
for e in e2:
|
146
|
-
e.lock = False
|
147
|
-
if winner:
|
148
|
-
e.user_id = winner.id
|
149
|
-
await session.commit()
|
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()
|
150
117
|
|
151
118
|
|
152
119
|
@egg_command_group.command(
|
@@ -160,7 +127,10 @@ async def connect4_command(
|
|
160
127
|
bet: app_commands.Range[int, 0] = 0,
|
161
128
|
) -> None:
|
162
129
|
"""Run a Connect4."""
|
163
|
-
|
130
|
+
members = (
|
131
|
+
(await random_members(ctx, bet))[:1] if member is None else [member]
|
132
|
+
)
|
133
|
+
await game_dual(ctx, bet, Connect4, *members)
|
164
134
|
|
165
135
|
|
166
136
|
@egg_command_group.command(
|
@@ -174,7 +144,10 @@ async def tictactoe_command(
|
|
174
144
|
bet: app_commands.Range[int, 0] = 0,
|
175
145
|
) -> None:
|
176
146
|
"""Run a tictactoe."""
|
177
|
-
|
147
|
+
members = (
|
148
|
+
(await random_members(ctx, bet))[:1] if member is None else [member]
|
149
|
+
)
|
150
|
+
await game_dual(ctx, bet, TicTacToe, *members)
|
178
151
|
|
179
152
|
|
180
153
|
@egg_command_group.command(
|
@@ -188,4 +161,44 @@ async def rockpaperscissor_command(
|
|
188
161
|
bet: app_commands.Range[int, 0] = 0,
|
189
162
|
) -> None:
|
190
163
|
"""Run a rockpaperscissor."""
|
191
|
-
|
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)
|