What is a Magic 8-Ball?
We can start by creating a new project in Pycharm called ‘Magic8’. Then let’s create a file called ‘magic8’. To begin our program we can start with the following code:
def main(): pass main()The 'pass' part of the code above might confuse you. What this command does is allow the programmer to not code the body of a function, conditional statement, or loop and have the program still compile and run fine. This command is useful when you are programming and testing as you go along.
Let's write the code for the 'main()' function. Drop in the following code above our function call:
def ask(): roll = int(random.random() * 3) + 1 if roll == 1: print("Yes!\n") elif roll == 2: print("No.\n") else: print("Maybe.\n") def main(): answer = True while answer: print("Welcome to the Magic 8-Ball!") print("Ask the 8-Ball a yes or no question and await your response: ") ask(raw_input()) print("Would you like to ask another question?") answer = check_input(raw_input().lower())Looking at the ‘main’ function above, we can see that we have a loop that runs forever. Inside the loop, we print out a welcome message and a request for a question from the user. Following that, we call the ‘ask’ function, however notice that we have a function call to ‘raw_input’. Remember that ‘raw_input’ is a built in function that allows the user to enter in a string. It’s important to note that this line of code will first evaluate the argument of the function ‘ask’ before calling ‘ask’. In other words, the interpreter will first ask the user to enter in a string, it will then pass that string as an argument to the ‘ask’ function. The control flow of the program will jump up to the definition of the ‘ask’ function.
Taking a look at the definition for the ‘ask’ function, we can see that it simply picks a random number from 1-3 based on that, either prints out ‘yes’, ‘no’ or ‘maybe’. Notice that the user’s input is stored in a variable ‘question’, however ‘question’ is not used in the function definition. This is because at this point in our program, we don’t care about the question since our Magic 8-Ball simply needs to answer ‘yes’, ‘no’ or ‘maybe’. Further on in this lab, we will make use of the ‘question’ variable when we add some intelligence into our program.
Let’s look at the last portion of our ‘main’ function. After printing out a random answer, the Magic 8-Ball program asks the user if they would like to ask another question and uses the method ‘lower’ to force the string the user typed in to be in all lowercase. This is done so that our branch statement checking the input is simplified. Next, the lowercase string is passed to the ‘check_input’ function.
Now Try
- Write the ‘check_input’ function in such a way that it checks if the user entered ‘yes’ and returns ‘True’.
- If the user entered in 'No', return ‘False’.
- If the user types in anything else, print out an error message and ask the user to re-enter their input.
Run your Magic 8-Ball program a few times and become familiar with it because we are going to continue modifying it.
Refining ‘check_input’
Currently, our main function asks the user for a question and asks if the user would like to continue asking questions. In a perfect world, the user would answer as we, the programmer, expect, however it is very possible that the user would enter in a blank line or a string of numbers. Following our algorithm, we can see that our program would accept that blank line or number and answer randomly with a ‘yes’, ‘no’ or ‘maybe’. This isn’t how our program should behave! Our program should print something along the lines of ‘Hey! Don’t enter a blank line, ask me a question!’ In order to do that, we need to take into account input that isn’t what is expected. We are going to handle the bad input by using a loop and a branch. Take a look at the following code block:
# Checks if user inputs are out of range def check_input(): while True: response = raw_input().lower() if response == '': print("You need to ask a question!") elif '?' not in response: print("Every question ends with a question mark!") else: return response ... def main(): print("Welcome to the Magic 8-Ball!") answer = True while answer: print("Ask the 8-Ball a yes or no question and await your response: ") check_input() ask()
Looking at our ‘main’ function, we can see that there has been a slight modification and the call to ‘raw_input’ has been moved to the ‘check_input’ function. Looking further, into our ‘checkInput’ function, we can see that there is a loop that asks the user for an input. Following the input, there is a branch that checks if the input was empty or the response contained a ‘?’. If the user simply hit ‘enter’ or didn’t use a question mark, then the program will respond appropriately. If neither of these conditions are true, then the program will move forward and return the user’s response to the ‘main’ function.
You might be asking yourself, why we are returning ‘response’ in ‘check_input’? At the moment we are not using the user’s question, but we will when we program some intelligence further along in our program.
Generalizing ‘check_input’
Our ‘check_input’ handles bad input when the user is prompted to ask a question, however, what about when the user is asked if they want to continue asking questions? Let’s generalize ‘check_input’ so that it can handle bad input in both cases. Take a look at the following code:
def main(): print("Welcome to the Magic 8-Ball!") answer = True while answer: print("Ask the 8-Ball a yes or no question and await your response: ") check_input(“question”) ask()If you notice, our call to ‘check_input’ now passes the string ‘question’ as an argument to the function definition. The reason we are doing this is so that our ‘check_input’ function will be able to run two different code blocks depending on whether it is handling bad input for when the user is asked for a question or when the user is asked whether they want to continue. Let’s now take a look at ‘check_input’:
def check_input(choice): while True: response = raw_input().lower() if choice == "again": #Your algorithm goes here. elif choice == "question": if response == '': print("You need to ask a question!") elif '?' not in response: print("Every question ends with a question mark!") else: return response
We can see that now the ‘check_input’ function definition has a parameter(the value that was passed to the function call as an argument) that is used to enter into a branch statement. The second branch handles the case if the we are checking the user’s input for when they enter a question and the first branch handles the input for when the user is asked if they want to continue. Also notice invocation to the the ‘lower’ method which forces the input the user types in to be in lowercase. This makes life simpler because then we don’t need to check if the user entered in ‘yes’ or ‘Yes’, we could simply check if they typed in ‘yes’. Alternatively, we could have used the method ‘upper’.
Now Try
- Write the logic for the first branch of the conditional statement.
- Be sure to check if the user entered blank line, ‘yes’, if they didn’t enter ‘yes’ or ‘no', or if they entered ‘no’. Your program should respond appropriately.
Adding ‘Intelligence’
At the moment, our algorithm answers positively or negative depending on the user’s input, however let’s get a bit more personal and answer in a way that gives the impression our program is intelligent. Intelligence is defined as “the ability to acquire and apply knowledge and skills.” This is where artificial intelligence differs from the type of 'intelligence' found in a typical chatbot. Artificial Intelligence uses grammar rules, vocabulary definitions and intending meaning to create a database of vocabulary words and phrases from which a system of logic is devised and extended over time. Such a system can be found in Google's TensorFlow(open source AI). Our Magic 8-Ball will not be using this sort of complex learning algorithm, instead it will fool the user by searching for keywords which will anticipate the user’s question and generate a typical response. Chatbots use a database of keywords and responses to convey the illusion of intelligence, however they are not acquiring nor applying knowledge and skills. We are going to add this functionality to our Magic 8-Ball program.
Take a look at the following modifications to the ‘main’ function:
def main(): answer = True while answer: print("Welcome to the Magic 8-Ball!") print("Ask the 8-Ball a yes or no question and await your response: ") ai(check_input(“question”)) ask() print("Would you like to ask another question?") answer = check_input(“again”)Notice that I have a nested set of function calls starting with ‘check_input’, whose return value is sent to ‘ai’, whose return value is then sent to ‘ask’ as an argument. ‘ask’ has not changed and simply rolls a random number and responds accordingly based on that number. Let’s take a look at what the ‘ai’ function does:
# Answers intelligently to the following questions: # Will i get an 'A' in this class? # Will i do good in this class? # Is this class hard? def rand_grade(choice): if choice == 'a': print("Getting a grade in the excellent range? Magic 8-Ball replies with a...") elif choice == 'well': print("Based on your mindset this year, the Magic 8-Ball says...") elif choice == 'hard': print("Is this class very difficult? ") elif choice == 'easy': print("Is this class going to be a piece of cake? ") # Answers questions about your grade def get_a(response): if 'will' in response and 'i' in response and 'get' in response and ("an 'a'" in response or 'an a' in response): rand_grade('a') elif 'will' in response and 'i' in response and 'do' in response and ("good" in response or 'well' in response): rand_grade('well') elif 'this' in response and 'class' in response and ("hard" in response or "difficult" in response): rand_grade('hard') elif 'this' in response and 'class' in response and "easy" in response: rand_grade('easy') def ai(response): get_a(response)Taking a look at the algorithm above, we can see that the ‘ai’ function runs ‘get_a’ which checks if the user entered in a set of keywords from which we can infer the meaning and the determine an appropriate response. Looking at the ‘get_a’ function, we can see that it uses the ‘in’ keyword to determine if a set of words is found in the question. We can easily deduce that if a question has the words ‘will’, ‘i’, ‘get’, ‘an’ and ‘a’, that the user is most likely asking if they will be getting an ‘A’ in a course of study. If this condition is found to be true, then the function ‘rand_grade’ is run while being passed the string ‘a’. Jumping to the function ‘rand_grade’, we can see that if the argument is the string ‘a’, then an appropriate response is printed to the user. From the user’s point of view, the program seemed to show signs of intelligence with a specific response to their question!
Looking at the other branches in the function ‘get_a’, we can see that they are checking for keywords in sentences that are similar in meaning. Following the flow of control to the function ‘rand_grade’, we can see that each branch then sends a string to ‘rand_grade’ which outputs an appropriate response.
Now Try
- Add another function call to the function definition for ‘ai’ which will anticipate and respond to a series of questions related to the success of the love life of your peers. Be sure to write a comment describing which questions are being anticipated.
Adding Some Flavor
Now you should have a Magic 8-Ball program that answers any question with a yes, no or maybe. If the 8-Ball is asked a question about a student’s grade or about the success of the Titan’s varsity football team, your algorithm will respond intelligently. However, if several different people run your program multiple times while asking questions with the same set of keywords, you will notice that your algorithm will respond with the same answers. Let’s fix that.
We can adjust for this by modifying our ‘ai’ function. Instead of simply calling the ‘get_a’ function, we will also generate a random number from 1 to 3 and depending on this number, your algorithm should respond to the same question differently. Add the following line of code to your program:
def ai(response): num = int(random.random() * 3) + 1 get_a(response, num)Notice that in addition to generating a random number, we also need to pass ‘get_a’ that random number. Now jumping to the definition for ‘get_a’, we need to add the following code:
# Answers questions about your grade def get_a(response, num): if 'will' in response and 'i' in response and 'get' in response and ("an 'a'" in response or 'an a' in response): rand_grade('a', num) elif 'will' in response and 'i' in response and 'do' in response and ("good" in response or 'well' in response): rand_grade('well', num) elif 'this' in response and 'class' in response and ("hard" in response or "difficult" in response): rand_grade('hard', num) elif 'this' in response and 'class' in response and "easy" in response: rand_grade('easy', num)You can see that we pass the random number generated in the function ‘ai’ to ‘get_a’ and finally to ‘rand_grade’. Let’s now take a look at ‘rand_grade’ to see what we are going to do with this random number:
def rand_grade(choice, num): if choice == 'a': if num == 1: print("Getting a grade in the excellent range? Magic 8-Ball replies with a...") elif num == 2: print("A perfect score in the class? Wouldn't that be wonderful. Magic 8-Ball says...") else: print("Hmmm... getting an 'A'? That's tough, Magic 8-Ball says...")Looking at the adjustment to ‘rand_grade’, you can see that we take the random number that was generated and have a different response for each possible number 1, 2 or 3.
Now Try
- Finish writing different responses for the other three variations on the same question. Also, apply the same technique and add random responses to your answers regarding your peer’s love lives.
Spellchecking w/ Enchant
Let’s add a spell checking functionality to our program so that when the user types in a sentence with spelling errors, we can output something like “I’m sorry, I don’t understand that. Can you type that in again?” You must understand that Python is heavily supported and has a large group of developers who create modules that can do most of the things you could ever think to do.
For example, Python has a module that can scan your least favorite person’s Facebook posts and write them to a file. From there, you can write a program that will scan them for any obnoxious statements or inconsistencies that you can later use to paint them in a less than flattering light. The module we are going to be using is called ‘Enchant’. ‘Enchant’ spell checks text and we are going to use it to spell check the input from our users. Enchant is already installed on the computers in the lab, however if you want to download it for your personal computer, a simple Google search will serve you well.
We start by adding the following code to the top of our program:
from enchant.checker import SpellCheckerNow we can use the spell checker in our programs by adding an additional branch to our ‘check_input’ function:
elif spell_check(response) > 0: print("I didn't understand that. Try again:")Understand that this branch makes absolutely no sense at the moment. In order to understand how this branch works, we need to take a look at the ‘spell_check’ function. Add this code to your program:
def spell_check(response): count = 0 chkr = SpellChecker("en_US") chkr.set_text(response) for err in chkr: if err: count += 1 return countTaking a look at the ‘spell_check’ function, we can see that it takes in the question from the user as a parameter. A ‘SpellChecker’ object is created that uses a US version of the English dictionary. Next, the method ‘set_text’ is invoked while being passed the user’s question. ‘set_text’ runs the spell check by comparing every word in the argument ‘response’ with the entries in the US dictionary. All words that don’t show up in the dictionary are placed into an array in the ‘SpellChecker’ object called ‘chkr’. Next, we have a for loop that runs through the words that are misspelled and if there is a misspelled word, the count variable is incremented. Lastly, the count variable is returned, which takes us back to the branch in our ‘check_input’ function. The expression in the branch will be equal to the return value of ‘spellCheck’, which will be greater than 0 only if there are spelling errors in the question submitted by the user. In such a case, the program will print out an appropriate message to the user and ask them to enter another question. Voila! We now have a program that spell checks the questions that the user enters!
Now Try
- It gets dull getting the same response to a mis-spelled question. Let’s add a bit of randomness and respond differently if a user misspells a word multiple times in a single session with the Magic 8-Ball.
Same Question
We have a Magic 8-Ball that now has a bit of personality. Excellent! Another modification we can add is a personal response if the user tries to be a smarty pants and ask the Magic 8-Ball the same question twice. The illusion of intelligence will be broken if our algorithm responds with the same answer repeatedly wouldn’t it? We can start by adding the following array to our main function:
def main(): repeat = [0] * 7 print("Welcome to the Magic 8-Ball!") answer = True while answer: print("Ask the 8-Ball a yes or no question and await your response: ") ai(check_input("question"), repeat) ask() print("Would you like to ask another question?") answer = check_input("again")On the first line of the ‘main’ function you will see something that should be confusing to you. This is a fancy way to declare an array named ‘repeat’ that has 8 elements. Each element in the array is also being initialized to 0. You may be asking yourself why are we creating an array of size 8? Well, this array will be used to keep track of how many times each of the 8 questions we are providing ‘intelligent’ responses to have been repeated by the user.
For example, the first element ‘repeat[0]’ will be incremented from 0 to 1 when the first question has been asked. It will then be incremented to 2 if the same question has been asked again. This will occur for each of the 8 questions and will be kept track of by each element in the array ‘repeat’.
Let’s put our ‘repeat’ array to use! Modify your code for the ‘ai’ function like so:
def ai(response, repeat): num = int(random.random() * 3) + 1 getA(response, num, repeat)Notice, that we have been creating variables in the main function and passing them as arguments from function to function. You may have been asking yourself, ‘Why not just create them as global variables and not deal with the headache of passing arguments and receiving parameters. The short answer is because global variables are evil. They should be avoided at all cost. Let’s leave it at that for now and plant the seed. We will see this seed of understanding grow to maturity when we begin to create games using Pygame. At that time, I will speak more on this not so obvious truth.
Getting back to our program, let’s make the following modification to our ‘getA’ function:
def get_a(response, num, repeat): if 'will' in response and 'i' in response and 'get' in response and ("an 'a'" in response or 'an a' in response): rand_grade('a', num, repeat, 0)Notice that we define ‘repeat’ as a parameter and when we call ‘rand_grade’, we pass ‘repeat’ as an argument along with the number ‘0’. ‘0’ signifies the index of the ‘repeat’ array that this question will use to count the number of times it’s been repeatedly asked by the user. Our three other questions will pass the arguments 1, 2 and 3 for the same reason.
Now Try
- Add the arguments for each of other cases in ‘getA’. Be sure that you are passing a different index as an argument to each call to ‘rand_grade’.
def rand_grade(choice, num, repeat, index): repeat[index] += 1 if check_repeat(repeat, index): print_repeat(num) if choice == 'a': if num == 1: print("Getting a grade in the excellent range? Magic 8-Ball replies with a...") elif num == 2: print("A perfect score in the class? Wouldn't that be wonderful. Magic 8-Ball says...") else: print("Hmmm... getting an 'A'? That's tough, Magic 8-Ball says...")Also add the following functions ‘check_repeat’ and ‘print_repeat’:
def check_repeat(repeat, index): if repeat[index] > 1: return True else: return False def print_repeat(num): if num == 1: print("Didn't you ask that question already? Anyhow...") elif num == 2: print("Is this the second time you are asking that? Geez...") elif num == 3: print("Asking the same question again? Alrighty...")Let’s take a look at what’s going on here. We have defined the repeat array and index as parameters to the function ‘rand_grade’. The first instruction we give is to increment the array at the specified index which represents the question at hand. Next, we have an if statement that has a function call as its conditional expression.
This brings up the fact that a function call is an expression that needs to be evaluated to a value. Sometimes that value is ‘None’, other times its an integer, float or string. In our case, the value that ‘check_repeat’ returns is ‘True’ or ‘False’. Placing a function call in a conditional expression is a shortcut that programmers use and is completely valid as long as the function call returns a boolean value.
The function ‘check_repeat’ checks if the appropriate index in the array ‘repeat’ is greater than 1, which means that the question has been asked more than once. If this is so, the ‘print_repeat’ function is called which prints out an appropriate remark before giving a response. Depending on whether it’s the first, second or third question that is asked, we now give the user an incentive to not repeat themselves and more importantly, we maintain the illusion of intelligence.
Project
Partner with a classmate and create a robust Magic 8-Ball that will give the following prompt:
"I am the Magic 8-Ball! Ask me any question about...<you fill this in>!"
Your Magic 8-Ball must answer 4 categories of questions on the topic of your choice. Each
question must anticipate 4 different ways to ask the same question. You must follow the guide
and give random responses to each anticipated question. You must also give random responses when the user
asks the same question.
We will end this project with a gallery walk and the top two Magic 8-Ball's will earn extra credit.
Add-Ons
Take a look at the videos below to get a blueprint on how to add additional elements to your project.
Examples
Take a look below to see examples of the project in an outstanding range.