This tutorial walks through the use of a binary tree algorithm in a discord chat bot by making the Animal Guessing Game. This tutorial assumes you have the Hello World discord bot from the building bots tutorial.
You can see a working example of this code on replit here: https://replit.com/@andoncemore/Guessing-Bot-Example#main.py
class Node:
def __init__(self):
# initialize our classclass Node:
def __init__(self):
self.value = ""
self.answer = ""
self.children = []class Node:
def __init__(self, value, answer="", children=[]):
self.value = value
self.answer = answer
self.children = children# Our Node Class Code
class Node:
def __init__(self, value, answer="", children=[]):
self.value = value
self.answer = answer
self.children = children
# Create an example Binary Tree
option1a = Node("Dog", answer="Yes")
option1b = Node("Wolf", answer="No")
option1 = Node("Is it a pet", answer="Yes", children=[option1a, option1b])
option2 = Node("Lizard", answer="No")
root = Node("Is it a mammal?", children=[option1,option2])Now that we have our Binary Tree represented in code, let's try to use the Root node to ask the first question. In the on_message function, we send a message with the "value" attribute of root, which is the first question we need to ask.
@client.event
async def on_message(message):
if message.author == client.user:
return
if(client.user in message.mentions):
await message.channel.send(content=root.value)Next to let the user respond to the the question, let's create a view with the response options.
from discord.ui import View, Button
class GuessOptionsView(View):
def __init__(self):
super().__init__()
# the callback function we will use to handle button press
async def handleButtonPress(self, interaction):
# do something once button is pressedfrom discord.ui import View, Button
class GuessOptionsView(View):
def __init__(self, node):
super().__init__()
for child in node.children:
self.add_item(Button(label=child.answer))
# the callback function we will use to handle button press
async def handleButtonPress(self, interaction):
# do something once button is pressedWe also need to link a callback function to each of the buttons. The easiest way to do this is to create a custom Button class. (The reason we can't directly use the handleButtonPress as our callback is that it won't know which button was pressed.)
class GuessButton(Button):
def __init__(self, node):
self.node = node;
super().__init__(label=node.answer)class GuessButton(Button):
def __init__(self, node):
self.node = node;
super().__init__(label=node.answer)
async def callback(self, interaction):
await self.view.handleNode(interaction, self.node)Button(label="") to GuessButton(child) and 2) Update our handleButtonPress function to accept a node.from discord.ui import View, Button
# Our Button Code from above
class GuessButton(Button):
def __init__(self, node):
self.node = node;
super().__init__(label=node.answer)
async def callback(self, interaction):
await self.view.handleNode(interaction, self.node)
class GuessOptionsView(View):
def __init__(self, node):
super().__init__()
for child in node.children:
# 1. Replace Button with GuessButton
self.add_item(GuessButton(child))
# 2. Update our handleButtonPress to accept a Node
async def handleButtonPress(self, interaction, node):
# do something once button is pressedasync def handleButtonPress(self, interaction, node):
await interaction.response.send_message(content=node.value, view= GuessOptionsView(node))@client.event
async def on_message(message):
if message.author == client.user:
return
if(client.user in message.mentions):
await message.channel.send(content=root.value, view= GuessOptionsView(root))Before we continue, let's test what we have so far:
from discord.ui import View, Button
# Our Node Class Code
class Node:
def __init__(self, value, answer="", children=[]):
self.value = value
self.answer = answer
self.children = children
# Create an example Binary Tree
option1a = Node("Dog", answer="Yes")
option1b = Node("Wolf", answer="No")
option1 = Node("Is it a pet", answer="Yes", children=[option1a, option1b])
option2 = Node("Lizard", answer="No")
root = Node("Is it a mammal?", children=[option1,option2])
# Our Button Code
class GuessButton(Button):
def __init__(self, node):
self.node = node;
super().__init__(label=node.answer)
async def callback(self, interaction):
await self.view.handleNode(interaction, self.node)
# Our Guess Options View
class GuessOptionsView(View):
def __init__(self, node):
super().__init__()
for child in node.children:
self.add_item(GuessButton(child))
async def handleButtonPress(self, interaction, node):
await interaction.response.send_message(content=node.value, view= GuessOptionsView(node))
# ... Discord Bot Setup Code Goes Here
# ...
# ...
# ...
@client.event
async def on_message(message):
if message.author == client.user:
return
if(client.user in message.mentions):
await message.channel.send(content=root.value, view= GuessOptionsView(root))When we get to the leaf node, we want to make a final guess instead of asking a question. We can edit our handleButtonPress function in the GuessOptionsView to do something different if we are at a leaf node.
async def handleButtonPress(self, interaction, node):
if(node.children == []):
# If it's a leaf node, make a guess
else:
# Else ask the next question
await interaction.response.send_message(content=node.value, view= GuessOptionsView(node))
async def handleButtonPress(self, interaction, node):
if(node.children == []):
await interaction.response.send_message(content=f'Are you thinking of a {node.value}?')
else:
# Else ask the next question
await interaction.response.send_message(content=node.value, view= GuessOptionsView(node))class WrongView(View):
def __init__(self, node):
super().__init__()
self.node = node
@discord.ui.button(label="You are wrong")
async def buttonCallback(self, interaction, button):
# Do something if they are wrongclass FeedbackModal(Modal):
def __init__(self, node):
self.node = node
super().__init__(title='Machine Learning')
self.newAnimal = TextInput(label="What animal were you thinking of?")
self.newQuestion = TextInput(label=f'A question to distinguish from {self.node.value}')
self.oldAnswer = TextInput(label=f'What answer gets {self.node.value}?')
self.newAnswer = TextInput(label="What answer gets your animal?")
self.add_item(newAnimal)
self.add_item(newQuestion)
self.add_item(oldAnswer)
self.add_item(newAnswer)
async def on_submit(self, interaction):
await interaction.response.send_message(f'Thanks! Algorithm is updating....{self.newAnimal.value}')async def on_submit(self, interaction):
newChildren = [Node(self.node.value, answer=self.oldAnswer.value), Node(self.newAnimal.value, answer=self.newAnswer.value)]
self.node.value = self.newQuestion.value
self.node.children = newChildren
await interaction.response.send_message(f'Thanks! Algorithm is updating....{self.newAnimal.value}')class WrongView(View):
def __init__(self, node):
super().__init__()
self.node = node
@discord.ui.button(label="You are wrong")
async def buttonCallback(self, interaction, button):
await interaction.response.send_modal(FeedbackModal(self.node))With everything put together, here's what you should have:
Take a look at replit for a working example. Here's all the code we just wrote, put together:
import discord
from discord.ui import View, Button, Modal, TextInput
import os
## Node Class
class Node:
def __init__(self, value, answer="", children=[]):
self.answer = answer
self.value = value
self.children = children
## Setup Example Tree
option1a = Node("Dog", answer="Yes")
option1b = Node("Wolf", answer="No")
option1 = Node("Is it a pet", answer="Yes", children=[option1a, option1b])
option2 = Node("Lizard", answer="No")
root = Node("Is it a mammal?", children=[option1,option2])
## GuessOptionsView Class
class GuessOptionsView(View):
def __init__(self, node):
super().__init__()
for option in node.children:
self.add_item(GuessButton(option))
async def handleNode(self, interaction, node):
# if there's no children, then it's the last node. Make the guess
if(node.children == []):
await interaction.response.send_message(content=f'Is it a {node.value}?', view=WrongView(node))
# otherwise, ask the next question.
else:
await interaction.response.send_message(content=node.value, view=GuessOptionsView(node))
## GuessButton Class
class GuessButton(Button):
def __init__(self, node):
self.node = node;
super().__init__(label=node.answer)
async def callback(self, interaction):
await self.view.handleNode(interaction, self.node)
## WrongView Class
class WrongView(View):
def __init__(self, node):
super().__init__()
self.node = node
@discord.ui.button(label="Wrong")
async def buttonCallback(self, interaction, button):
await interaction.response.send_modal(FeedbackModal(self.node))
# FeedbackModal Class
class FeedbackModal(Modal):
def __init__(self, node):
self.node = node
super().__init__(title='Machine Learning')
self.question = TextInput(label=f'A question to distinguish from {node.value}')
self.animal = TextInput(label="What animal were you thinking of?")
self.newAnswer = TextInput(label="And what is the answer to the question?")
self.currentAnswer = TextInput(label=f'What answer gets {node.value}?')
self.add_item(self.question)
self.add_item(self.animal)
self.add_item(self.newAnswer)
self.add_item(self.currentAnswer)
async def on_submit(self, interaction):
newChildren = [Node(self.node.value, answer=self.currentAnswer.value), Node(self.animal.value, answer=self.newAnswer.value)]
self.node.value = self.question.value
self.node.children = newChildren
await interaction.response.send_message(f'Thanks! Algorithm is updating....{self.animal.value}')
## Discord Bot Basic Setup
intents = discord.Intents.default()
intents.message_content = True
client = discord.Client(intents=intents)
@client.event
async def on_ready():
print(f'We have logged in as {client.user}')
@client.event
async def on_message(message):
if message.content.startswith('$hello2'):
await message.channel.send(content=root.value, view=GuessOptionsView(root))
token = os.getenv("DISCORD_BOT_SECRET")
client.run(token)It's a bit annoying to have to initialize our decision tree by manually creating Nodes like we did above for our testing purposes. I've created a helper function that can parse a "script" file and initialize the tree.
Caveat: This will only work if you use the same Node class as I've defined above.
The script format looks like this. The structure of the tree is parsed automatically based on the number of tabs - so each line MUST be tabbed over to the appropriate amount for the tree to be created.
Is it an animal?
Yes, Is it a mammal?
Yes, Cheetah
No, Snake
Maybe, Coral
No, ChairThe following is the parse function that can read in a script file:
def parseScript(filename):
# function that takes a list of lines, and returns the root node
def createNode(lines):
# if the length of lines is 1, Create an End Node
if(len(lines) == 1):
content = lines[0].strip('\t\n').split(',')
return Node(content[1].strip(), answer=content[0].strip())
# Create a list of all the children nodes
level = lines[0].count("\t") + 1
index = 1;
children = []
start = 1
while(index+1 < len(lines)):
index += 1
if(lines[index].count("\t") <= level):
children.append(lines[start:index])
start = index
children.append(lines[start: len(lines)])
content = lines[0].strip('\t\n').split(',')
if(len(content) == 1):
# Create a Root Node
return Node(content[0], children=[createNode(child) for child in children])
else:
# Create a Middle Node
return Node(content[1].strip(), answer=content[0].strip(), children=[createNode(child) for child in children])
file = open(filename, "r")
content = file.readlines()
file.close()
return createNode(content)To use the function, just pass in the path to the script file like so:
root = parseScript("script.txt")