Friday, December 12, 2014

Building a Dynamic Multiple-Choice Question in LiveCode

This post is a follow-up to the one I posted on December 7, 2014. My goal here is to begin explaining the mechanics of how I built the dynamic multiple-choice question app that I described in that previous post (here also is a link to the video I included in that post). Consequently this post is "code heavy."

For starters, by "dynamic" I don't mean "Wow!" or "Fantastic!" Rather, I mean that the app is not static. That is, I don't just copy and paste cards for each question in the quiz. Instead, the questions are generated from a back-end database of questions, which in this case is just a text field. Although there can be dozens, hundreds, or even thousands of questions, there is only one card that acts as the "question engine." Indeed, the entire stack consists of only three cards titled as follows: home, ask question, and content.

The questions are stored in a field titled "content" on a card also titled "content." Yes, I know, you are confused already. I probably should have given these separate names, but this serves as a nice reminder that you can have the same name for different LiveCode objects. Besides, this makes remembering these important labels much easier. For the rest of this post, I'll refer to this field on this card as the question bank. Here is a screen snapshot of the card with this scrolling field:



As explained in my previous post, the questions and answers are entered using the following format:
  1. On the first line enter the line number of the correct answer (using the line of the first answer as line number 1). If you want a particular question to override the "Randomize Answers" option, then add a comma and enter "off" (example: 3,off).
  2. Enter the question stem on the next line.
  3. Enter as many answers as you wish, up to five. Put each answer on a separate line.
  4. After the last answer, on a separate line, write the word "end."
Here's an example:

4,off
What color is a stop sign?
green
yellow
blue
red
violet
end

Notice that I dimmed out ",off" in the first line. The reason is that I've dimmed all lines of script in this post that relate to the "randomize answers" function of the app. I'll explain how that function works in a separate blog post. Likewise, I will completely ignore them in the lines of script and discussion that follow. But, I figured you would want to know they are there. Again, don't worry, I'll explain them fully in a subsequent post.

The "Ask Question" Card Script


The card titled "Ask Question" is best thought of as the "multiple-choice question engine." So, we will dissect this card to understand how it works. First, here is all of the code on the card that executes when the card is opened, plus the related global variables that are also declared on the card script:

global gQuestion, gCorrectAnswer, gChoice1, gChoice2, gChoice3, gChoice4, gChoice5, gLine, gAnswerChosen, 

global gTotalCorrect, gTotalWrong, gPercentage, gQuestionNumber, gTotalQuestions
global varRandomizeAnswers, gRandomOption

on OpenCard
   if the hilite of button "randomize answers" on card "content" is true then
      put true into varRandomizeAnswers
   else
      put false into varRandomizeAnswers
   end if
   put 0 into gTotalCorrect
   put 0 into gTotalWrong
   put 0 into gPercentage
   put 0 into gQuestionNumber
   put 1 into gLine
   show field "stem"
   show field "feedback"
   hide field "Done"
   put empty into field "Question Number"
   put empty into field "Number Correct"
   put empty into field "Number Wrong"
   put empty into field "Percentage"
   put the number of lines of field "content" of card "content" into L
   put 0 into q
   //check to see how many questions there are at the start of the quiz
   //See the text field on the card "content" - each question must end with the word "end" on a separate line
   repeat with i = 1 to L
      if line i of field "content" of card "content" = "end" then add 1 to q
   end repeat
   put q into gTotalQuestions
   nextQuestion
end OpenCard

The first five lines initialize the main variables: gTotalCorrect, gTotalWrong, gPercentage, gQuestionNumber, and gLine. (I'm using the prefix "g" here to note the fact that they are all "global variables." As I mentioned in my previous post, I actually built this project many months ago and I was using this naming convention then. Now, I tend just to use the prefer "var" to denote "variable," particularly for global variables.)

The next seven lines deal with various fields on the card. The fields "stem" and "feedback" refer to, respectively, as the stem of the question and the feedback given to the user. The field "Done" is only shown at the end of the quiz, so that field is hidden at the start of the quiz. The next group of fields comprise the scoreboard that appears at the top of the card.

The next line checks for the number of lines in the question bank:

   put the number of lines of field "content" of card "content" into L

I like to use "L" to signify "number of lines." It is a local variable, which I like to use for variables that are used and then immediately discarded. So, this line simply counts the number of lines in the question bank and puts that number into L.

The next group of code comprises a loop that "counts" the number of question by going through each line of the question bank, one line at a time, looking for the word "end." If found, the script adds one to another local variable named "q" (short for "questions"). When the loop is over (by running out of lines), it puts that number in q into the global variable gTotalQuestions.

The last line triggers a custom command I built called "nextQuestion."

The nextQuestion Command


The nextQuestion command is also defined on the card script (I put this just above the "openCard" script described above). It's somewhat lengthy, but don't worry, we'll make sense out of it:

on nextQuestion
   put empty into field "feedback"
   hide button "Continue"
   hide button "choice1" 
   hide button "choice2" 
   hide button "choice3" 
   hide button "choice4" 
   hide button "choice5" 
   set the hilite of button "choice1" to false
   set the hilite of button "choice2" to false
   set the hilite of button "choice3" to false
   set the hilite of button "choice4" to false
   set the hilite of button "choice5" to false
   
   //Put the various parts of the next question into the variables used on this card
   //Variable for number of the correct answer choice:
   put item 1 of line gLine of field "content" of card "content" into gCorrectAnswer
   put item 2 of line gLine of field "content" of card "content" into gRandomOption 
   add 1 to gLine
   //Quiz is over if the next line in the text field containing the questions is empty
   if line gLine of field "content" of card "content" is empty then 
      show field "Done"
      hide field "Feedback"
      hide field "stem"
      exit nextQuestion
   end if
   add 1 to gQuestionNumber
   put "Question: "&gQuestionNumber&" of "&gTotalQuestions into line 1 of field "Question Number"
   //Variable for the question itself
   put line gLine of field "content" of card "content" into gQuestion
   put 0 into L
   repeat until choice = "end"
      add 1 to gLine
      add 1 to L
      put line gLine of field "content" of card "content" into choice
      //Variables for the individual answers, up to 5. Loop ends when it 'runs out of answers.'
      //Just add more gChoice# variables if you want to increase the maximum number of possible answers.
      if l =1 then put line gLine of field "content" of card "content" into gChoice1
      if l =2 then put line gLine of field "content" of card "content" into gChoice2
      if l =3 then put line gLine of field "content" of card "content" into gChoice3
      if l =4 then put line gLine of field "content" of card "content" into gChoice4
      if l =5 then put line gLine of field "content" of card "content" into gChoice5
   end repeat
   add 1 to gLine
   put gQuestion into card field "stem"
   set the label of button "choice1" to gChoice1
   set the label of button "choice2" to gChoice2
   set the label of button "choice3" to gChoice3
   set the label of button "choice4" to gChoice4
   set the label of button "choice5" to gChoice5
   
   //show only the buttons that have answers in them for the current question
   put 320 into x
   put 130 into y
   
   //populate varAnswerList with number of items equal to the number of answers
   //l-1 equals the number of answers
   //This repeat does the same thing as putting items into a field
   //Below you will see that I set the item delimiter to the space character
   repeat with i = 1 to l-1
      put i&space after varAnswerList
   end repeat
   
   //Reminder that l-1 equals the number of answers for this question
   repeat with i = 1 to l-1
      put "Choice"&i into localButton      
      //Randomize location of button choices
      if varRandomizeAnswers is true and "off" is not among the words of gRandomOption then
         set the itemDelimiter to space
         put number of items of varAnswerList into j
         put random(j) into varItem
         put item varItem of varAnswerList into k
         delete item varItem of varAnswerList
         put "Choice"&k into localButton
      end if
      set the itemDelimiter to comma
      set the location of button localButton to x,y
      show button localButton
      add 30 to y
   end repeat
end nextQuestion

The first line "puts empty" into the field "feedback." This just makes sure that any feedback from the previous question has been removed.

Next, the five buttons that comprise the answers are first hidden. The reason is that we only want to reveal the number of answers that are needed - questions are allowed to contain any number of answers, up to five. All of these buttons are defined as "radio" buttons, so the next five lines make sure that the radio button in each is not highlighted. (Since I first created this app, I have since learned how to copy and paste objects, such as buttons. So, I could revise this app to allow a question to have as many answers as would fit on the screen. But, for now, I think having an upper range of five answers is fine.)

Next, we need to begin constructing the question. Recall that we initialized the global variable "gLine" in the openCard script by setting it to 1. Also recall that the first line of each question contains the number of the correct answer. Using the stop sign example, the correct answer is red, which is the fourth answer choice give. Hence 4 is the first item in line 1.

Next, I add 1 to gLine and take a look to see what is there. If it is empty, we can safely assume that there are no more questions and we can end the quiz by showing the "Done" field and doing a few other things, including exiting out of the nextQuestion command. If it is not empty, this command keeps chugging along. And, if it is not empty, keep in mind that gLine now equals 2.

The next thing it does is "add 1 to gQuestionNumber." The variable gQuestionNumber started out as 0, so it becomes 1 for the first question (then 2 for the second question, etc.).

The next line shows some key information in the scoreboard at the top of the card, namely which question out of how many is currently being shown:

put "Question: "&gQuestionNumber&" of "&gTotalQuestions into line 1 of field "Question Number"

The key text and variable information are concatenated together and inserted into the field "Question Number."

Given that gLine equals 2, the next line takes that line from the question bank and puts it into the variable "gQuestion":

   put line gLine of field "content" of card "content" into gQuestion

gQuestion holds the entire text of the question.

Getting the Answers and Making Each the Label of a Different Button


Next we have another loop that repeats for as many answers as there are for the question. The trigger to end the loop is whether or not the line contains the word "end." You can also see that I reuse the local variable L. The loop sets the variable L equal to 1 and it begins counting as it finds more answers. You will notice that one is also added to the global variable "gLine" as it goes because it is important to keep going through each line of the question bank in successive order.

So, as this loop progresses, the variable L begins takes each answer and puts it into another global variable associated with it, as shown in the first of the five lines:

      if l =1 then put line gLine of field "content" of card "content" into gChoice1

The next few lines set the label for each of the buttons equal to the respective answer. (Each button has a name and a label. So, when defining buttons, you can name it something that will not change, but you can then alter the label as you wish. If there is a label for a button defined, the user will only see the label, not the name. It will be important in a moment to remember that the names of the buttons are "Choice1," "Choice2," "Choice3, etc.)

Next, we need to reveal the buttons for which we have answers. I start this by setting the location on the screen where I want the first button to appear. I do this using two other local variables "x" and "y":

   put 320 into x
   put 130 into y
   
(Note here: I used this strategy because of the randomizing option. But, I figured I'd explain this now.)

Finally, I have a loop that reveals each button in order, starting with button "Choice1." I have another local variable i that is equal to the number of the button. So, if I concatenate (i.e. join together) "Choice" and "1", I get the number of the button. I put that name into another local variable "localButton":

  put "Choice"&i into localButton

Then, the button is placed at x,y. It is then revealed. 30 is then added to y so that the next answer will be placed 30 pixels below:

      set the location of button localButton to x,y
      show button localButton
      add 30 to y

The result is the display of the question:


Checking to See if the User's Answer is Correct


After the question and all the answers are presented, the user now can choose an answer. So, the following script is in each of the answer buttons:

global gAnswerChosen

on mouseUp
   put the name of me into gAnswerChosen
   checkAnswer
end mouseUp

The "name of me" simply equals the name of the button chosen. So, if the user selects option 1, the following text is put into the global variable "gAnswerChosen":

   button "Choice1"

Notice that the 15th character happens to be the number 1. For button "Choice2," the 15th character is 2. This fact becomes important shortly. 

The only other line of code is the command "checkAnswer," which is yet another custom function I created. Let's explore that.

The checkAnswer Command


The script for this command is also on the card:

on checkAnswer
   //This procedure checks to see if the answer is correct. If not, feedback is given with the correct answer.
   //Everything is automated based on the contents of the question bank on the card "content."
   //This approach only works well if the answers are relatively short. Long answers would be awkward.
   if gCorrectAnswer = 1 then put gChoice1 into varAnswerText
   if gCorrectAnswer = 2 then put gChoice2 into varAnswerText
   if gCorrectAnswer = 3 then put gChoice3 into varAnswerText
   if gCorrectAnswer = 4 then put gChoice4 into varAnswerText
   if gCorrectAnswer = 5 then put gChoice5 into varAnswerText
   //character 15 is the number associated with the button, as in: button "choice1"
   put character 15 of gAnswerChosen into x
   if x = gCorrectAnswer then 
      put "That's correct!" into line 1 of field "feedback"
      add 1 to gTotalCorrect
   else
      put "Sorry, that's not correct. The correct answer is "&varAnswerText&"." into line 1 of field "feedback"
      add 1 to gTotalWrong
   end if
   put gTotalCorrect/(gTotalCorrect+gTotalWrong)*100 into gPercentage
   put "Question: "&gQuestionNumber&" of "&gTotalQuestions into line 1 of field "Question Number"
   put "Correct: "&gTotalCorrect into line 1 of field "Number Correct"
   put "Wrong: "&gTotalWrong into line 1 of field "Number Wrong"
   put round(gPercentage)&"% Correct" into line 1 of field "Percentage"
   show button "Continue"
end checkAnswer

The first thing this command does is check to see which is the correct answer, which has been conveniently stored for some time now in the global variable "gCorrectAnswer." It then puts the text of the correct answer in the variable "varAnswerText." This variable will come in handy later if the user chooses the wrong answer and needs feedback.  

The next line - "put character 15 of gAnswerChosen into x" - looks for that all-important 15th character of gAnswerChosen, which contains the name of the button. If the 15th character is a number that matches x, then the answer chosen is correct, which the following IF-THEN-ELSE construction handles. The last lines of the command update the various fields that comprise the scorecard at the top of the card.

So, if the answer chosen is incorrect, the user if given feedback and the scoreboard updates accordingly:



Gee, I Must Be Really Smart to Have Figured This Out


As I reread this post and see how much code is displayed, I realize that many of you who don't know LiveCode well yet might be impressed. Well, don't be. Should anyone think I must be really smart to have figured all this out, I can tell you there are many people standing by who would gladly point out the opposite. It reminds me of when I was really young and didn't know how to write in cursive. I remember watching my older sister writing in this "strange code" and thinking how smart she must be because she could do something I couldn't. A lot of us also remember how awe-inspiring it was to watch someone use a manual transmission expertly and with ease while we were desperately trying to "crack the code" of shifting (especially while navigating the hills of Pittsburgh). Funny thing is, after we learned these skills, the awesomeness melted away and we weren't very impressed anymore. Well, it's the same with coding. The hard part - and the interesting part - is coming up with any idea worth building.


No comments:

Post a Comment