#python #asynchronous #async-await #discord.py #python-asyncio
#питон #асинхронный #асинхронный-ожидание #discord.py #python-асинхронный
Вопрос:
Описание
Прежде всего, я довольно новичок в разработке async / await, особенно с Python. На данный момент я разрабатываю бота Discord, в котором игроки могут выполнять slots <bet>
. Отображается вставка с отредактированными смайликами.
При первом вызове команды встраивание отображает вращающийся gif-смайлик, который затем «редактируется», чтобы через несколько секунд стать настоящим смайликом (чтобы создать впечатление, что слоты «вращаются»):
После его завершения игрок может выбрать функцию «повторить попытку», чтобы повторить процесс снова. Итак, в целом, поток таков:
- Игрок вызывает команду «слот»
slots <bet>
- появляются 3 «вращающихся» смайлика, и через 0,5 секунды 1 после другого переключается на «статическую» версию
- Игрок может нажать кнопку «повторить попытку», и процесс повторяется.
Код
utilities.py
import asyncio
from functools import wraps, partial
def sync_to_async(func):
@wraps(func)
async def run(*args, loop=None, executor=None, **kwargs):
if loop is None:
loop = asyncio.get_event_loop()
pfunc = partial(func, *args, **kwargs)
return await loop.run_in_executor(executor, pfunc)
return run
slotmachine.py
import asyncio
import random
from enum import Enum
from typing import Union
from discord import Color, Embed, Message, Reaction, User
from discord.ext.commands import Bot, Context
from utilities import sync_to_async
import arrow
class SlotMachine:
class Reel(Enum):
BANANA = "🍌"
CHERRY = "🍒"
GRAPE = "🍇"
DOLLAR = "💵"
MONEY = "🤑"
symbols = list(Reel)
animated_emoji = "<a:slots:765434893781041183>"
row_of_three_payout = {
Reel.BANANA: 2,
Reel.CHERRY: 4,
Reel.GRAPE: 8,
Reel.DOLLAR: 15,
Reel.MONEY: 20,
}
row_of_two_payout = {
Reel.BANANA: 1,
Reel.CHERRY: 2,
Reel.GRAPE: 4,
Reel.DOLLAR: 0,
Reel.MONEY: 0,
}
def __init__(self, ctx, bot, user, coins, bet):
self.ctx = ctx
self.bot = bot
self.user = user
self.bet = bet
self.profit = 0
self.initial_coins = coins
self.coins = coins
self.message = None
@sync_to_async
def has_row_of_three(self, slots: list) -> bool:
if all(i == slots[0] for i in slots):
return SlotMachine.row_of_three_payout[slots[0]]
@sync_to_async
def has_row_of_two(self, slots: list) -> bool:
if slots[0] == slots[1]:
return True
elif slots[1] == slots[2]:
return True
else:
return False
async def calculate_payout_multiplier(self, slots: list):
has_three, has_two = await asyncio.gather(
self.has_row_of_three(slots), self.has_row_of_two(slots)
)
if has_three:
return SlotMachine.row_of_three_payout[slots[0]]
elif has_two:
return SlotMachine.row_of_two_payout[slots[1]]
else:
return 0
@sync_to_async
def generate_embed(self, embed_color, multiplier, slots):
embed = Embed(title=f"Slots 🎰", color=embed_color)
embed.add_field(
name="Spin",
value=f"{' | '.join(slots)}",
inline=False,
)
multiplier = f"{multiplier}x" if multiplier > 0 else "None"
embed.add_field(name="Multiplier", value=multiplier)
embed.add_field(name="Profit", value=self.profit)
embed.add_field(name="Coins", value=self.coins)
embed.add_field(name="Again?", value="React with 🔁 to play again.")
embed.set_footer(text=self.user, icon_url=self.user.avatar_url)
return embed
async def play(self):
slots = [random.choice(SlotMachine.symbols) for _ in range(3)]
payout_multiplier = await self.calculate_payout_multiplier(slots)
return (self.bet * payout_multiplier, payout_multiplier, slots)
@sync_to_async
def generate_spin_embed(self, display) -> Embed:
embed = Embed(title=f"Slots 🎰", color=Color.from_rgb(0, 0, 0))
embed.add_field(
name="Spin",
value=" | ".join(display),
inline=False,
)
embed.set_footer(text=self.user, icon_url=self.user.avatar_url)
return embed
async def spin_animation(self, slots) -> None:
display = [SlotMachine.animated_emoji] * 3
embed = await self.generate_spin_embed(display)
await self.message.edit(embed=embed)
for i in range(len(slots)):
await asyncio.sleep(0.5)
display[i] = slots[i].value
embed = await self.generate_spin_embed(display)
await self.message.edit(embed=embed)
def is_valid_reaction(self, reaction: Reaction, user: User):
return (
user == self.user
and str(reaction.emoji) in ("🔁")
and reaction.message.id == self.message.id
)
async def game_loop(self):
has_busted = False
is_first = True
self.message = await self.ctx.send(embed=Embed(title="⌛ Loading..."))
try:
while not has_busted:
money_out, multiplier, slots = await self.play()
self.profit = money_out - self.bet
self.coins = self.profit
await self.spin_animation(slots)
if self.coins <= 0:
self.coins = 0
has_busted = True
await self.message.edit(
embed=await self.generate_embed(
Color.red(), multiplier, [s.value for s in slots]
)
)
else:
if self.profit == 0:
embed_color = Color.from_rgb(0, 0, 0)
elif self.profit > 0:
embed_color = Color.green()
else:
embed_color = Color.red()
embed = await self.generate_embed(
embed_color, multiplier, [s.value for s in slots]
)
await asyncio.sleep(0.5)
await self.message.edit(embed=embed)
if is_first:
await self.message.add_reaction("🔁")
is_first = False
else:
await self.message.remove_reaction("🔁", self.user)
if self.bet > self.coins:
has_busted = True
else:
await self.bot.wait_for(
"reaction_add", timeout=30, check=self.is_valid_reaction
)
# They busted at this point
amount = self.bet if self.bet > self.coins else self.initial_coins
await self.bot.api.modify_gambling_profit(
user_id=self.user.id, game="slots", amount=-1 * amount
)
except asyncio.TimeoutError:
if self.initial_coins > self.coins:
amount = -1 * (self.initial_coins - self.coins)
else:
amount = self.coins - self.initial_coins
await self.bot.api.modify_gambling_profit(
user_id=self.user.id, game="slots", amount=amount
)
commands.py
@commands.command(name="slots", aliases=["slot"])
@commands.cooldown(1, 5, commands.BucketType.user)
async def slots_command(self, ctx: Context, bet: int) -> None:
user_id = ctx.author.id
if await self.currently_playing.get(f"gambling.{user_id}"):
await ctx.send("❌ Already in another game.")
return
else:
await self.currently_playing.set(f"gambling.{user_id}", 1)
if not await self.is_valid_bet(bet):
await ctx.send("❌ Bet must be a positive whole number.")
return
stats = await self.bot.api.get_economy_user(user_id=user_id)
coins = stats["coins"]
if await self.has_enough_coins(user_id, coins, bet):
await SlotMachine(ctx, self.bot, ctx.author, coins, bet).game_loop()
else:
await ctx.send("❌ Not enough coins")
await self.currently_playing.delete(f"gambling.{user_id}")
Проблема
Когда я тестировал с 1 человеком, это работало фантастически; однако, когда несколько человек выполняют одну и ту же команду «слоты», все получают огромное отставание. Это почти так, как если бы один слот ‘run’ ожидал другого.
Вопросы
- Кто-нибудь видит, что заметно не так с кодом? Есть ли что-то, что блокирует основной поток? Я так полагаю?
- Какие методы я могу использовать для отладки / расследования этой проблемы? В настоящее время я запускаю debug на asyncio, но он не выдает никаких предупреждений о
slots
команде?
На данный момент я настолько расстроен, что собираюсь перейти на более «асинхронный» дружественный язык, такой как go
или javascript
. Хотя это немного преувеличенная / начинающая мысль 😉
Комментарии:
1. Я бы рекомендовал избегать столь интенсивного (или любого другого, если вы не знаете, что делаете) использования
sync_to_async
. В вашем текущем коде совершенно неясно, какая функция действительно асинхронна, какая синхронизируется, но не блокируется, а какая синхронизируется и блокируется. Например, такие методы, какhas_row_of_two
definitely, не нуждаются в декораторе, они могут оставаться синхронизированными, и вы можете просто вызывать их в обычном режиме.