Sunday, January 25, 2015

LiveCode at UGA's Digital Learning Conference

The second annual Digital Learning Conference was held Friday and Saturday here in Athens at the University of Georgia. I did my best to have LiveCode well represented. It was a wonderful conference, thanks to all the organizers and volunteers, especially my colleagues Dr. Christa Deissler and Dr. Theodore (TJ) Kopcha. There were some amazing, high quality sessions, such as those by innovative folks like Cat Flippen and Gary Shattuck. Very impressive stuff.

I did a LiveCode workshop and also presented my video analysis tool project. Not too many people attended my workshop, but several people asked me about it during the conference. I also used this opportunity to create another LiveCode "first project" (i.e. a fun first project designed especially for people who have never programmed before). I called it "Travel the USA!" and I think it worked well.

The conference had a participatory theme. Presenters were encouraged to propose sessions that emphasized interaction and hands-on activities. This motivated me to create a beta tester web site for people interested in trying out my video analysis tool. I barely pulled off the development of this web site in time for my session, but it all worked well. I designed my one-hour session as a mini-workshop about my video analysis tool. And, I now have a way to easily give people access to the tool while also gathering some important usability data from people with real interest in doing video analysis. I used a delightful video called "A Toy Train in Space" to demonstrate my video analysis tool - check it out:


As always, I enjoy introducing people who have never programmed before to LiveCode and what you can do with it.



Saturday, January 17, 2015

Comparing Programming Approaches in LiveCode: An Order of Magnitude Difference?


I would like to say that this will be my most exciting, riveting, thrilling, hold-on-to-your-seat blog post yet. But, truth be told -- especially to the casual reader with little or no experience with LiveCode -- it may be the most boring one I've written. Of course, if you are a math nerd, perhaps the title piqued your interest a little with the mention of "order of magnitude." And I promise I will use this concept as the cornerstone of this posting. But, if that is the only thing that interests you, feel free to skip to the very end for a wonderful video about the powers of ten to make your decision to click your mouse or tap your screen to come here worthwhile.

Still here? OK, some quick background... In my previous post I mentioned how I received some helpful advice from Richard Gaskin a few months back suggesting that I could improve the performance of one of my projects if I were to take a different programming approach. Let me make it very clear at the start how grateful I am to Richard for taking the time to make this comment. This was a very kind gesture on his part. He was obviously under no obligation to respond, but took significant time to read through what I was working on and posted a very thoughtful comment. And, as I have googled other LiveCode questions and problems over the past two years, it's quite remarkable how many times I find an answer or suggestion given by Richard. He is a exemplary LiveCode citizen who is very willing to share his extensive expertise and who also seems to be constantly scanning the LiveCode world to try to help people out. I had the good fortune to meet him at RunRevLive 2014 in San Diego and he also seemed like a regular guy who was sincerely interested in whatever you were working on.

To briefly recap, I had been using text fields to handle list or data processing tasks and Richard wrote to say that I could dramatically speed things up if I would use variables instead. Here again is what Richard's wrote:
"...by using the "repeat for each" loop method combined with moving data out of the complex field structures into variables for use within the loop, this should bring your processing time down by at least an order of magnitude."
So yes, I've been thinking about this "order of magnitude" thing. I know Richard knows exactly what an "order of magnitude" means. In contrast, many people who throw this phrase around just kind of vaguely know it means "much more." I think a lot of other people think it means "twice as much." But, actually, an order of magnitude is used to compare things on a logarithmic scale, which most commonly uses base 10. The best example I can think is the Richter Scale, which is used to compare the size of earthquakes. An order of magnitude is a step along the scale that uses the powers of 10. For example, consider the difference between 10^2, 10^3, and 10^4. (I can't easily show exponents in this blog, so I'm using the character ^ to mean "raised to the power of.") If written out, these numbers are 100, 1000, and 10000, respectively. The difference between these three values is not equal, but instead increases exponentially. In fact, I think the best way to think of something measured on a logarithmic scale is to go in descending order, that is, from 10000 to 1000 to 10. At each step, you are going 90% the remaining distance! If you take a really huge number, such as 10^100 (known as a googol, by the way), you eliminate 90% when you go to 10^99. Going to 10^98 likewise removes 90% of that remaining amount, and so on.

So, for Richard to make the assertion that changing my programming approach would improve performance by at least an order of magnitude is really saying something - he's saying that the speed will increase tenfold. But the more I thought about it, I really wondered if this were true. So, I decided to test this hypothesis.

A LiveCode Programming Experiment


To test Richard's hypothesis, I quickly built a LiveCode app that performed a very simple operation: It takes a long list of words and transfers it from one "container" to another. In one version, the container is a text field. In a second version, the container is a variable. The idea is to see how long it takes for LiveCode to complete the process using both types of containers: 1) from one text field to the other text field; and 2) from one variable to the other variable.

And while that sounds simple enough, there are actually several other decisions we have to make. For example, should I take a word from the top of the first list and add it to the top of the second? Or, how about taking a word from the bottom of the first list and moving to the bottom of the second? And, there are quite a few more variations I could throw in. Let's start with what I think would be the most grueling for LiveCode to perform: taking a word from the top of the first list and moving it to the top of the second (thus deleting it from the first list). I'll call this experiment 1.

Here is the code for this loop:

   put the number of lines in field "field1" into L
   repeat with i = 1 to L
      put line 1 of field "field1"&return before field "field2"
      delete line 1 of field "field1"
      put the number of lines in field "field1" into field "field1 total"
      put the number of lines in field "field2" into field "field2 total"

   end repeat

I titled the two fields "field1" and "field2," with the words being transferred from the first to the second. This loop repeats for as many words (one word per line in the field) as there are in the field. I always take the first word (on line 1) from field1, so you will see "line 1" listed twice above: first to "put" the word into field2, then to delete the word.

The following line above puts that word into the first line of field2:

      put line 1 of field "field1"&return before field "field2"

As you can see, I also take advantage of the "before" keyword. This keyword very conveniently lets you put something at the beginning of a list of items without having to worry about how to move every other item. (A similar keyword is "after," which I talk about below.) It's also important to concatenate a "return" so that the word solely occupies the first line in field2.

The reason why I call this the most "grueling" approach for LiveCode to accomplish is that the words shown in the two fields are constantly shiftly, as shown in the video below.

The final two lines (before the "end repeat") are used to show a running tally of the number of words in each field as the loop executes. Depending on the experiment being performed, this can be the only feedback given to us that something is actually "working." So, I hope you can see that there is not many lines of code here.

Now, let's take a look at the related code for moving the words from one variable to another.

   put the number of lines in varField1 into L
   repeat with i = 1 to L
      put line 1 of varField1&return before varField2
      delete line 1 of varField1
      put the number of lines in varField1 into field "field1 total"
      put the number of lines in varField2 into field "field2 total"

   end repeat

Your first reaction should be "Whoa, isn't this the same code?" Well, it's almost the same. Instead of referring to the two fields, I simply substituted the local variables "varField1" and "varField2". The important point is that this code processes the words exactly the same between the two examples, except for what containers are used to store the words.

If you are still confused as to what is going on here, watch a short movie I made demonstrating the app:

[ Get the free LiveCode Community version. ]



OK, let's take a look at the results of this first experiment.

Experiment 1 Results


Here are the results from the first experiment (time given in seconds):

Fields
Variables
move before and delete
141.72 54.59


As these data show, using variables instead of fields for this task is 2.6 times faster when the words are moved to the top of the second field the AND the words from the first field are deleted as they are moved. Yes, that is a big difference, but it is not an order of magnitude difference (i.e. 10 times faster). So, a preliminary conclusion is that Richard is correct in saying that the use of variables will be processed faster, but he overestimated the magnitude.

Experiment 2 Results


OK, let's do another experiment. What if we move the words from the first container to the second, but not delete them from the first. Accomplishing this difference is simply a matter of not including line 4 that begins with the word "delete." This does not seem to me to require as much processing power because you are doing less with the first field. But, who knows, perhaps some other dynamic is in play here that I'm not aware of. It's definitely worth exploring. Here are the results:

Fields
Variables
move before
73.59 27.30

In this case, using variables instead of fields for this task is 2.7 times faster when the words are moved to the top of the second field, but not deleted from the first field. This is a tad faster, but I'll call it a tie. I am a little surprised that the difference is not lower.


Two More Experiments: 1) Move After and Delete; and 2) Move After, But Don't Delete


I performed two other experiments where I used the keyword "after" instead of "before." As the word implies, this means that each word was added to the second field at the end of the field instead of at the beginning. From a processing standpoint, after the words fill the field in the second field you don't actually see the words being added to the field as they are "below" the bottom edge of the field. So, this would imply that LiveCode can process this more quickly than putting them before. I'll share the data in a moment, but there were no big surprises here. But, I did yet another experiment which did surprise me.

One Last Experiment: What Happens When You Hide the Fields


Watching the program execute this list processing task in all of the previous experiments, particularly when the words are moved to the top of the second field and deleted from the first, it seems clear that moving the words from field to field takes quite a bit of processing power. You can just see the difference. (Actually, I find it quite mesmerizing.) I wondered, though, what would happen if the fields were hidden from view? So, one more experiment was in order.

This last experiment was pretty simple. It's the same as the first experiment, but with both fields hidden. The data from this second experiment were very surprising. There was virtually no difference in processing time between using fields and using variables!

Here is a table with all of the data from all of the experiments:

Fields
Fields
Variables
Visible
Hidden
move before
73.59 27.29 27.30
move before and delete
141.72 54.60 54.59
move after
27.94 27.30 27.29
move after and delete
100.78 54.59 54.57


So, In Conclusion...


My main conclusion is that it doesn't seem to matter if you use fields or variables as the containers for list processing tasks such as the one used here, given the very important condition that the fields are not visible.

But on the Other Hand...


Of course, I might have completely misunderstood Richard's advice and therefore set up a perfectly useless experiment. That's OK, I've wasted time in much more uninteresting ways. But, I am open to any feedback that Richard or any other LiveCode expert may want to give me here. I'm even willing to revise my programming experiment to test other approaches, assuming I can figure out how to code it. Or, better yet, you could download the file yourself and revise it to test your own hypotheses - be sure to let me know what you find.

Many thanks again to Richard Haskin for making me think and for motivating me to explore this programming question. This is exactly the kind of feedback I was hoping for from experts in the LiveCode community when I began writing this blog. 

Powers of 10 Video


I end this blog posting (most of which I'm sure many of you wisely skipped) by sharing what might be one of my most favorite science videos of all time. It's called simply "Powers of 10," and it is narrated Phillip Morrison, a famous physicist and science educator who taught at MIT and worked on the Manhattan project. This will truly give you an idea of what is meant by an "order of magnitude."



Sunday, December 21, 2014

Randomizing the Display of Multiple-Choice Question Options with LiveCode

As promised, in this post I explain how I randomized the answer options in my multiple-choice question application. This post is really just a continuation of my previous post where I explained the mechanics of how I built the dynamic multiple-choice question app, so be sure to read that one before going any further. In fact, I highly recommend you start by reading my first post that provides an overview of this project. You might also find it useful to watch a short video I made showing how the application works.

To begin, recall from my previous post that I deliberately dimmed out all of the lines of script that dealt with this feature. In this post, I have highlighted those very same lines in red.

Let's start with the "Ask Question" card script. (And yes, this posting is also quite "code heavy.")

The "Ask Question" Card Script


Here again is all of the 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

First, notice the two global variables that are associated with the randomizing option: varRandomizeAnswers, gRandomOption

The first - varRandomizeAnswers - is used to signal if the randomizing option is on or off. The first five lines of code in the openCard script check to see if the check box titled "randomize answers" on the card "content" (containing the question bank) is checked or not (recall this check box is labeled for the user as "Randomize Answers During Quiz").

   if the hilite of button "randomize answers" on card "content" is true then
      put true into varRandomizeAnswers
   else
      put false into varRandomizeAnswers
   end if

So, if it is checked (that is, the "hilite is true"), then we put true into the varRandomizeAnswers variable. Otherwise, it gets set to false. As we look at the other scripts related to this randomizing option below, notice how it will only be executed if this variable is true.

As you can see, this is the only code that within the openCard script that deals with the randomizing option. Next we need to take a look at the custom command "nextQuestion," which is triggered by the last line of the openCard script.

The nextQuestion Command


Here again is the entire script:

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

I'll explain later why some lines are shown in green. The first red line of code to consider is the following:

   put item 2 of line gLine of field "content" of card "content" into gRandomOption

Recall that gLine begins with 1, then increments through the entire question bank as the quiz proceeds. The first item in the first line for each new question contains the correct answer for that question. However, recall that we have the option to include the word "off" as the second item on this line. If the word "off" appears, it signals that the randomizing option should be ignored for that question.

It's probably helpful to refresh our memory of how a question must formatted, so here again are the rules, followed by an example:

  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."
     4,off
     What color is a stop sign?
     green
     yellow
     blue
     red
     violet
     end

So, item 2 of this first line is stored in the other global variable associated with the randomizing option - gRandomOption. (If the word "off" does not appear, then this variable would simply contain the value of "empty.") We'll see how this variable is used shortly.

Random Drawing from a Hat: A Concrete Example to Understand the Concept


Before we dive into the code, I suggest you first take some time to understand conceptually what the code is trying to do. Let's use the trite but useful example of pulling numbers at random out of a hat. So, imagine that you write numbers on slips of paper - as many slips of paper as there are answer choices -- and put them all into a hat. Then, with your eyes closed, pull out one of the slips of paper at random from the hat.

So, using the question example above on what is the color of a stop sign, we would write the numbers 1 to 5 each on a slip of paper. OK, let's imagine that the first number drawn is 2. That would mean that the first answer choice is yellow. Next - and this is extremely important - we throw away that slip of paper. That is, we deliberately do NOT put it back into the hat. Well, of course! We only can show that answer choice once for the question.

So, we are now left with the numbers 1, 3, 4, and 5 in the hat. Let's imagine that 4 is the next number chosen - which happens to be the correct answer - and so we show the answer option red next. Following this logic, we can then imagine that 5, then 3, then 1 are chosen. So, the answer options would appear as follows:

  • yellow
  • red
  • violet
  • blue
  • green

Also take note that we need to remember that the correct answer is 4 (i.e. red), which just happens to be in the second answer slot. Confused? Actually, I'm betting you are following this, but I'm sure you can see that we have a lot of small details to keep track of, including the fact that questions can have anywhere between two and five answers. And that's a good place to begin.

Fortunately, we already have a local variable, L, that is keeping track of the number of answers for the current question. Actually, the number of answers is L-1. So, mirroring the idea of writing numbers on as many slips of paper as there are answers, we need to generate L-1 numbers. How to do this? If you've read any postings from previous projects I've built, you know that I am very fond of creating what I've called "shadow fields," or fields that are hidden from view but which perform small list processing functions. So, my temptation was to create such a field, then create a loop that would put a unique number on each line, starting with one and ending with the total numbers of answers. But, I decided on a different strategy due to some advice by a LiveCode expert, Richard Gaskin, that he wrote about one of my projects from the summer that used the "shadow field" strategy:
"...by using the "repeat for each" loop method combined with moving data out of the complex field structures into variables for use within the loop, this should bring your processing time down by at least an order of magnitude."
Before I do anything else, let me just say that this is exactly the kind of feedback I was hoping for from experts in the LiveCode community when I began writing this blog. Anyhow, I've been thinking about Richard's comment for some time and have decided to implement his advice here.

Richard's point wasn't that my use of fields was wrong, only slow. The speed at which certain scripts execute can make a big difference in the overall performance of the application. A faster method is by and large a better method. So, instead of creating a shadow field, I've created a little loop that repeats L-1 times and builds a "hat with slips of paper in it" inside a variable named "varAnswerList":

   repeat with i = 1 to L-1
      put i&space after varAnswerList
   end repeat

It starts with 1 and puts the number of the loop plus a space into a local variable named "varAnswerList." Notice that it actually puts this value "after" instead of "into" varAnswerList. This means that the contents of each loop will be added to whatever is already in the variable. So, let's walk through the loops associated with five answer choices (I'll spell out "space" to make it visible):

      1space
      1space2space
      1space2space3space
      1space2space3space4space
      1space2space3space4space5space

So, when finished, varAnswerList contains "1 2 3 4 5 " (notice the spaces), kind of like a hat holding slips of paper.

Get Ready, Get Set, Randomize!


The next set of red lines is the heart of the randomizing option, however notice that these are part of another loop shown in green. To keep you from getting neck and finger strain from excessive scrolling, I've copied and pasted that code here:

   //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

The best way to understand the randomizing is to first understand what happens if no randomizing of answer choices takes place. That is, let's start by understanding what happens if the red lines are ignored. I've already explained the mechanics of this in my previous blog posting, but a refresher here is in order. Notice that the red lines of code are all contained within a repeat loop shown by the green lines. As we walk through the green lines, remember that the buttons corresponding to the five answer buttons are titled "Choice1," "Choice2," etc. (Remember also that buttons can have both a title and a label. In this app, the titles do not change, but the labels do - the labels correspond to the answer choices given to the user for that question.)

The repeat loop repeats L-1 times, which we already know is the number of answer choices for the current question. Each loop is numbered by i, beginning with 1. So, the second line says to put "Choice" plus i into a local variable named localButton. Therefore, for the first loop, localButton equals "Choice1." Skipping the red lines, the next green line sets the location button localButton (in this case button "Choice1") to the screen coordinates x,y, which were defined in previous code. Then, the button is made visible by the command "show." Then, 30 is added to y so that the next answer button will appear 30 pixels below the current answer button. So, if we ignore the red lines, this repeat loop will display the answer choices in order: Choice1, then Choice2, then Choice3, and so on.

Can We Please Randomize Now?


OK, let's explore those red lines! Basically, the red lines will override the sequential ordering of the buttons by identifying a new value for the variable localButton.

Let's dissect the red lines slowly. Notice that all of this script is contained within an if/then code block. The condition is whether varRandomizeAnswers is true AND if the word "off" is present within the variable gRandomOption. Now, it is possible that the person who set up the question bank may have typed "off" with spaces before or after. Therefore, in order to account for this I'm using a very convenient operator named "is not among." This is really nice because it does the hard work of looking for the consecutive string of letters "off" within the variable gRandomOption. If these consecutive letters are among whatever LiveCode finds there, then, of course, nothing else in the if/then code block would executive because the if condition would not be met.

But, let's now assume that the all is true in order to see how the rest of this if/then code block works.

First, I set the itemDelimiter property to the space character. By default in LiveCode, it is set to a comma. So, why change it? Well, recall that the variable varAnswerList uses spaces between the numbers of answer choices. I need to be able to refer to each of these numbers as separate "items" (i.e. separate pieces of paper) and since I used spaces, not commas, I need to make sure the itemDelimiter property can "see" them separately.

This begs the question of why didn't I just use commas instead of spaces when I wrote that code? The answer brings me back to Richard Gaskin. When I attended the RunRevLive conference in San Diego back in September 2014, I met Richard. One day I sat at his table for lunch while he was speaking very passionately about why commas should be eradicated from data file structures, such as within .csv files (he apparently has written a popular article titled something like "CSV Must Die!" Not much room for nuance there. The short answer to the long explanation he gave is simply that commas are much too common a symbol to use for such a purpose. (He actually recommends the use of tabs.)

I've been using commas throughout all of my LiveCode projects and have become quite fond of them. But, his words have been floating around in my head for awhile. So, I thought I would turn over a new leaf and begin to practice better coding practices, hence my use of spaces, not commas, here. Onto the next line of code:
put number of items of varAnswerList into j
This line stores the number of items in the variable varAnswerList in the local variable j. This is equal to the slips of paper currently in the hat. You might be asking why I just didn't write "put L-1 into j", but recall that we will throw each slip of paper away as we go, so the number of items in varAnswerList (i.e. our hat), will decrease by one for each green loop. Moving on...
put random(j) into varItem
put item varItem of varAnswerList into k 
The action of these two lines is equivalent to drawing a slip of paper at random from the hat. The number on that slip is put into yet another local variable k. To be honest, I could have combined these two lines into one:
put item random(j) of varAnswerList into k
The next line is the equivalent of throwing away the slip of paper after it has been drawn from the hat:
delete item varItem of varAnswerList
 If we imagine that 2 was the number drawn, then varAnswerList would now contain "1 3 4 5 ".

Finally, this line overrides its green line counterpart:
put "Choice"&k into localButton
That is, instead of "Choice"&i this line uses "Choice"&k. Yes, there is a big difference between i and k!

Finally, I reset the itemDelimiter back to a comma. Why? Recall that I use a comma to separate the correct answer from the word "off." I think that is a good use of a comma as the delimiter because it is easy for the user to recognize. So, by resetting it back to a comma here I ensure that that line of code will work the next time it is encountered. This underscores how great it is to be able to change the delimiter to whatever you want during a LiveCode project.

But, If the Answers Are Randomized, How Does LiveCode Still Know Which Is the Correct Answer?


Yes, this sounds like a dilemma, but it's actually a non-issue because even though the answer choice buttons are randomly shown, recall that the title of each still contains the original number of the button. So, the button "Choice4" may be the second button shown, but we can still refer to its button title to know that it is the correct answer to the question of what color is a stop sign.

Randomized Answers as the Default Option for the Quiz


I think having the quiz display the answers for each question in random order should be the default option for the quiz. To do this, I put the following code on the stack script:

on openstack
   set the hilite of button "randomize answers" on card "content" to true
end openstack

As this code implies, when the stack is first opened, it checks the button "randomize answers" on the content card. This means that unless the user deliberately goes to that card and unchecks this buttons, the questions will be shown with randomly ordered answers.

So, What's Next?


I began this little project by contrasting the use of multiple-choice questions within LiveCode, Adobe Captivate, and Articulate Storyline. I argued that an advantage of LiveCode is that multiple-choice questions could be part of any software design, including gaming, whereas their use is tightly constrained within Captivate and Storyline, usually resulting in the time worn and weary eLearning strategy of "present, practice, quiz." If I continue working with this multiple-choice question app, I intend to explore its use in a game. I know, you can hardly wait.


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.