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,
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
put false into varRandomizeAnswers
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:
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
It's probably helpful to refresh our memory of how a question must formatted, so here again are the rules, followed by an example:
- 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).
- Enter the question stem on the next line.
- Enter as many answers as you wish, up to five. Put each answer on a separate line.
- After the last answer, on a separate line, write the word "end."
What color is a stop sign?
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:
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
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):
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:
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 jThis 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
put item random(j) of varAnswerList into kThe next line is the equivalent of throwing away the slip of paper after it has been drawn from the hat:
delete item varItem of varAnswerListIf 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 localButtonThat 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.