tl;dr
我的机器人如何异步等待多条消息的反应?
我正在向我的 Discord 机器人添加石头剪刀布 (rps) 命令。用户可以通过输入调用命令.rps
以及一个可选参数,指定要玩的用户。
.rps @TrebledJ
被调用时,机器人将直接向调用它的用户和目标用户(通过参数)发送消息 (DM)。然后这两个用户react使用 ✊、???? 或 ✌️ 发送给他们的 DM。
现在我正在尝试让它异步工作。具体来说,机器人将向两个用户发送 DM(异步)并等待他们的反应(异步)。分步场景:
Scenario (Asynchronous):
1. User A sends ".rps @User_B"
2. Bot DMs User A and B.
3. User A and B react to their DMs.
4. Bot processes reactions and outputs winner.
(See also: Note 1)
由于目标是监听等待多个消息的反应,因此我尝试创建两个单独的线程/池。以下是三种尝试:
multiprocessing.pool.ThreadPool
multiprocessing.Pool
concurrent.futures.ProcessPoolExecutor
不幸的是,这三件事都没有成功。 (也许我实施了一些不正确的事情?)
以下代码显示了命令功能(rps
),一个辅助函数(rps_dm_helper
),以及三次(不成功的)尝试。这些尝试都使用不同的辅助函数,但底层逻辑是相同的。为了方便起见,第一次尝试没有被注释。
import asyncio
import discord
from discord.ext import commands
import random
import os
from multiprocessing.pool import ThreadPool # Attempt 1
# from multiprocessing import Pool # Attempt 2
# from concurrent.futures import ProcessPoolExecutor # Attempt 3
bot = commands.Bot(command_prefix='.')
emojis = ['✊', '????', '✌']
# Attempt 1 & 2
async def rps_dm_helper(player: discord.User, opponent: discord.User):
if player.bot:
return random.choice(emojis)
message = await player.send(f"Playing Rock-Paper-Scissors with {opponent}. React with your choice.")
for e in emojis:
await message.add_reaction(e)
try:
reaction, _ = await bot.wait_for('reaction_add',
check=lambda r, u: r.emoji in emojis and r.message.id == message.id and u == player,
timeout=60)
except asyncio.TimeoutError:
return None
return reaction.emoji
# # Attempt 3
# def rps_dm_helper(tpl: (discord.User, discord.User)):
# player, opponent = tpl
#
# if player.bot:
# return random.choice(emojis)
#
# async def rps_dm_helper_impl():
# message = await player.send(f"Playing Rock-Paper-Scissors with {opponent}. React with your choice.")
#
# for e in emojis:
# await message.add_reaction(e)
#
# try:
# reaction, _ = await bot.wait_for('reaction_add',
# check=lambda r, u: r.emoji in emojis and r.message.id == message.id and u == player,
# timeout=60)
# except asyncio.TimeoutError:
# return None
#
# return reaction.emoji
#
# return asyncio.run(rps_dm_helper_impl())
@bot.command()
async def rps(ctx, opponent: discord.User = None):
"""
Play rock-paper-scissors!
"""
if opponent is None:
opponent = bot.user
# Attempt 1: multiprocessing.pool.ThreadPool
pool = ThreadPool(processes=2)
author_result = pool.apply_async(asyncio.run, args=(rps_dm_helper(ctx.author, opponent),))
opponent_result = pool.apply_async(asyncio.run, args=(rps_dm_helper(opponent, ctx.author),))
author_emoji = author_result.get()
opponent_emoji = opponent_result.get()
# # Attempt 2: multiprocessing.Pool
# pool = Pool(processes=2)
# author_result = pool.apply_async(rps_dm_helper, args=(ctx.author, opponent))
# opponent_result = pool.apply_async(rps_dm_helper, args=(opponent, ctx.author))
# author_emoji = author_result.get()
# opponent_emoji = opponent_result.get()
# # Attempt 3: concurrent.futures.ProcessPoolExecutor
# with ProcessPoolExecutor() as exc:
# author_emoji, opponent_emoji = list(exc.map(rps_dm_helper, [(ctx.author, opponent), (opponent, ctx.author)]))
### -- END ATTEMPTS
if author_emoji is None:
await ctx.send(f"```diff\n- RPS: {ctx.author} timed out\n```")
return
if opponent_emoji is None:
await ctx.send(f"```diff\n- RPS: {opponent} timed out\n```")
return
author_idx = emojis.index(author_emoji)
opponent_idx = emojis.index(opponent_emoji)
if author_idx == opponent_idx:
winner = None
elif author_idx == (opponent_idx + 1) % 3:
winner = ctx.author
else:
winner = opponent
# send to main channel
await ctx.send([f'{winner} won!', 'Tie'][winner is None])
bot.run(os.environ.get("BOT_TOKEN"))
Note
1 Contrast the asynchronous scenario to a non-asynchronous one:
Scenario (Non-Asynchronous):
1. User A sends ".rps @User_B"
2. Bot DMs User A.
3. User A reacts to his/her DM.
4. Bot DMs User B.
5. User B reacts to his/her DM.
6. Bot processes reactions and outputs winner.
这实现起来并不难:
...
@bot.command()
async def rps(ctx, opponent: discord.User = None):
"""
Play rock-paper-scissors!
"""
...
author_emoji = await rps_dm_helper(ctx.author, opponent)
if author_emoji is None:
await ctx.send(f"```diff\n- RPS: {ctx.author} timed out\n```")
return
opponent_emoji = await rps_dm_helper(opponent, ctx.author)
if opponent_emoji is None:
await ctx.send(f"```diff\n- RPS: {opponent} timed out\n```")
return
...
但恕我直言,非异步会导致糟糕的用户体验。 :-)