Discord.py 如何制作干净的对话树?
Posted
技术标签:
【中文标题】Discord.py 如何制作干净的对话树?【英文标题】:Discord.py How to make clean dialog trees? 【发布时间】:2021-10-15 05:11:29 【问题描述】:我的目标是清理我的代码,以便我可以更轻松地制作对话树,而无需不断复制不必要的片段。我可以在 python 中干净地做到这一点,但 discord.py 似乎有不同的要求。这是我当前非常冗余的代码示例:
if 'I need help' in message.content.lower():
await message.channel.trigger_typing()
await asyncio.sleep(2)
response = 'Do you need help'
await message.channel.send(response)
await message.channel.send("yes or no?")
def check(msg):
return msg.author == message.author and msg.channel == message.channel and msg.content.lower() in ["yes", "no"]
msg = await client.wait_for("message", check=check)
if msg.content.lower() == "no":
await message.channel.trigger_typing()
await asyncio.sleep(2)
response = 'okay'
await message.channel.send(response)
if msg.content.lower() == "yes":
await message.channel.trigger_typing()
await asyncio.sleep(2)
response = 'I have something. Would you like to continue?'
await message.channel.send(response)
await message.channel.send("yes or no?")
def check(msg):
return msg.author == message.author and msg.channel == message.channel and msg.content.lower() in ["yes", "no"]
msg = await client.wait_for("message", check=check)
if msg.content.lower() == "no":
await message.channel.trigger_typing()
await asyncio.sleep(2)
response = 'Okay'
await message.channel.send(response)
我尝试制作函数来处理重复代码,但没有成功。例如,使用:
async def respond(response, channel):
await channel.trigger_typing()
await asyncio.sleep(2)
await channel.send(response)
...
await respond(response, message.channel)
理想情况下,我希望能够为树对话框本身做这样的事情,就像在 python 中一样:
if __name__=='__main__':
hallucinated =
1:
'Text': [
"It sounds like you may be hallucinating, would you like help with trying to disprove it?"
],
'Options': [
("yes", 2),
("no", 3)
]
,
2:
'Text': [
"Is it auditory, visual, or tactile?"
],
'Options': [
("auditory", 4),
("visual", 5),
("tactile", 6)
]
【问题讨论】:
【参考方案1】:您的总体想法是正确的:可以用与您描述的结构相似的结构来表示这样的系统。它被称为finite state machine。我已经编写了一个示例来说明如何实现其中之一——这个特定的结构使用类似于interactive fiction 的结构,如Zork,但同样的原则也适用于对话树。
from typing import Tuple, Mapping, Callable, Optional, Any
import traceback
import discord
import logging
import asyncio
logging.basicConfig(level=logging.DEBUG)
client = discord.Client()
NodeId = str
ABORT_COMMAND = '!abort'
class BadFSMError(ValueError):
""" Base class for exceptions that occur while evaluating the dialog FSM. """
class FSMAbortedError(BadFSMError):
""" Raised when the user aborted the execution of a FSM. """
class LinkToNowhereError(BadFSMError):
""" Raised when a node links to another node that doesn't exist. """
class NoEntryNodeError(BadFSMError):
""" Raised when the entry node is unset. """
class Node:
""" Node in the dialog FSM. """
def __init__(self,
text_on_enter: Optional[str],
choices: Mapping[str, Tuple[NodeId, Callable[[Any], None]]],
delay_before_text: int = 2, is_exit_node: bool = False):
self.text_on_enter = text_on_enter
self.choices = choices
self.delay_before_text = delay_before_text
self.is_exit_node = is_exit_node
async def walk_from(self, message) -> Optional[NodeId]:
""" Get the user's input and return the next node in the FSM that the user went to. """
async with message.channel.typing():
await asyncio.sleep(self.delay_before_text)
if self.text_on_enter:
await message.channel.send(self.text_on_enter)
if self.is_exit_node: return None
def is_my_message(msg):
return msg.author == message.author and msg.channel == message.channel
user_message = await client.wait_for("message", check=is_my_message)
choice = user_message.content
while choice not in self.choices:
if choice == ABORT_COMMAND: raise FSMAbortedError
await message.channel.send("Please select one of the following: " + ', '.join(list(self.choices)))
user_message = await client.wait_for("message", check=is_my_message)
choice = user_message.content
result = self.choices[choice]
if isinstance(result, tuple):
next_id, mod_func = self.choices[choice]
mod_func(self)
else: next_id = result
return next_id
class DialogFSM:
""" Dialog finite state machine. """
def __init__(self, nodes=, entry_node=None):
self.nodes: Mapping[NodeId, Node] = nodes
self.entry_node: NodeId = entry_node
def add_node(self, id: NodeId, node: Node):
""" Add a node to the FSM. """
if id in self.nodes: raise ValueError(f"Node with ID id already exists!")
self.nodes[id] = node
def set_entry(self, id: NodeId):
""" Set entry node. """
if id not in self.nodes: raise ValueError(f"Tried to set unknown node id as entry")
self.entry_node = id
async def evaluate(self, message):
""" Evaluate the FSM, beginning from this message. """
if not self.entry_node: raise NoEntryNodeError
current_node = self.nodes[self.entry_node]
while current_node is not None:
next_node_id = await current_node.walk_from(message)
if next_node_id is None: return
if next_node_id not in self.nodes: raise LinkToNowhereError(f"A node links to next_node_id, which doesn't exist")
current_node = self.nodes[next_node_id]
def break_glass(node):
node.text_on_enter = "You are in a blue room. The remains of a shattered stained glass ceiling are scattered around. There is a step-ladder you can use to climb out."
del node.choices['break']
node.choices['u'] = 'exit'
nodes =
'central': Node("You are in a white room. There are doors leading east, north, and a ladder going up.", 'n': 'xroom', 'e': 'yroom', 'u': 'zroom'),
'xroom': Node("You are in a red room. There is a large 'X' on the wall in front of you. The only exit is south.", 's': 'central'),
'yroom': Node("You are in a green room. There is a large 'Y' on the wall to the right. The only exit is west.", 'w': 'central'),
'zroom': Node("You are in a blue room. There is a large 'Z' on the stained glass ceiling. There is a step-ladder and a hammer.", 'd': 'central', 'break': ('zroom', break_glass)),
'exit': Node("You have climbed out into a forest. You see the remains of a glass ceiling next to you. You are safe now.", , is_exit_node=True)
fsm = DialogFSM(nodes, 'central')
@client.event
async def on_message(msg):
if msg.content == '!begin':
try:
await fsm.evaluate(msg)
await msg.channel.send("FSM terminated successfully")
except:
await msg.channel.send(traceback.format_exc())
client.run("token")
这是一个示例运行:
【讨论】:
【参考方案2】:di = 'hallucinated':
1:
'Text': [
"It sounds like you may be hallucinating, would you like help with trying to disprove it?"
],
'Options': 'yes': 2, 'no': 3
,
2:
'Text': [
"Is it auditory, visual, or tactile?"
],
'Options':
"auditory": 4,
"visual": 5,
"tactile": 6
# Modified the dictionary a little bit, so we can get the option values directly, and the starter keywords.
def make_check(options, message):
def predicate(msg):
return msg.author == message.author and msg.channel == message.channel and msg.content.lower() in options
return predicate
# I noticed the check function in your code was repetitive, we use higher order functions to solve this
async def response(dialogues, number, message, client):
await message.channel.send(dialogues[number]['Text'])
options = [x[0] for x in dialogues[number]['Options']]
if options:
msg = await client.wait_for("message", check=make_check(options, message), timeout=30.0)
return await response(dialogues, dialogues[number]['Options'][msg], message, client)
else:
pass
# end dialogues
# Use recursion to remove redundant code, we navigate through the dialogues with the numbers provided
async def on_message(message):
# basic on_message for example
starters = ['hallucinated']
initial = [x for x in starters if x in message.content.lower()]
if initial:
initial_opening_conversation = initial[0]
await response(di.get(initial_opening_conversation), 1, message, client)
此代码应该可以正常工作,但您可能需要处理 wait_for
中的 TimeoutError,如果您的选项值不正确,它可能会进入无限循环。
【讨论】:
以上是关于Discord.py 如何制作干净的对话树?的主要内容,如果未能解决你的问题,请参考以下文章