I was going to make a pdf, but I think I'll just write it here for now.
Definitions:
Let's think about what defines a test question. You have the actual question itself, 4 available choices, and 1 correct answer. We can represent all of these variables in a single element. To do that, we define a UDT, or User-Defined Type.
Type TestQuestion
question as string
choice1 as string
choice2 as string
choice3 as string
choice4 as string
answer as integer
EndType
What's you've created your UDT, you can then use it to set it as a variable's data type. Data types are integer, float, string, etc...
When you say
x = 4, x is defined as an integer. If you want x to contain a decimal number, you need to make it a float by using the pound sign at the end of the variable like so:
x# = 4.0
This is the same as if you explicitly defined the variables like this:
x as integer
x = 4
or
x as float
x = 4.0
Note that if you define the variable explicitly, you don't need the symbols like # (or $ for a string). So define a variable with a UDT would look like this:
test as TestQuestion
Now, the variable
test can contain 6 different values, each with their own data type. And this is how you access those values:
test.question = "Ask me something?"
test.choice1 = "first answer choice"
test.answer = 2
All our test question will be structured into an array defined with our TestQuestion UDT.
Arrays:
An array is a list of items. What those items are depends on what data type you've defined for the array. In our case, it will be the UDT created above. Let's define an array of questions for a math test.
dim math(5) as TestQuestion
This defines
math as an array with 5 elements of type TestQuestion. Instead of doing something like this:
math1 as TestQuestion
math2 as TestQuestion
math3 as TestQuestion
You create the array with the number of elements you want. The more you start using arrays, the more you'll see the incredible benefit and flexibility it gives you. What if you had 100 questions? You really want to define 100 different variables separately? It certainly would not be very convenient to access them. Everything would have to be hard-coded, much like how your levels are currently constructed.
To access the different elements in the array, you use an index number.
math(1).question = "Some kind of question"
math(2).question = "More text here"
math(3).question = "What's my name?"
What can we do with this? It allows us to easily remove much of redundant code in your program. Look at each level in your project and you'll see a pattern. Each level has the same layout, the same text, the same controls. So why write it more than once? Again, what if you did this for 100 questions? What a nightmare! To simplify, we replace the hard-coded text with references from the array. Maintain a variable to track which question we're on, this will act as the index to the array. When you're ready to move onto the next question, increment that value.
Look over this code for displaying your test questions.
ink rgb(255,255,255),0 : set text size 30
center text 320,200, math(currentQuestion).question
set text size 15
text 240,280, "A.) " + math(currentQuestion).choice1
text 240,300, "B.) " + math(currentQuestion).choice2
text 240,320, "C.) " + math(currentQuestion).choice3
text 240,340, "D.) " + math(currentQuestion).choice4
Do you see how by incrementing
currentQuestion we can change the question on the screen without rewriting any code? Cool isn't it!
Footnote: When defining an array in DarkBasic, the index range is actually from 0 to size, thus the true size of your array is actually 1 more than the size you specified. You should not use that entire range as it would be bad practice since this is only present in DB and no other language. You should either use index values between 0 and size-1 or from 1 to size. Most languages start at 0 and go to size-1. It may be a good habit of getting used to that.
DATA command:
Above, I showed you how to access the values in a UDT and array so now you should be able to start writing and adding more test questions. However, there is another way of filling your array with questions and answers, and that is to use the DATA command.
This command allows you to create a series of data of any primitive type. Each chunk of data is separated by a comma or another data command. You can bunch it all up onto a single line, or put each chunk on a separate data command. It doesn't matter, so how you organize it should be based on what makes it easier for you to read and understand. You then access that data sequentially using the read command, which you use to assign those values to other variables. In this case we read the data into our array.
MathQuestions:
data 4
data "What is 2+2?", "2", "1", "3", "4", 4
data "What is 99+1?", "50", "60", "100", "98", 3
data "What is 56.1 + 82.3?", "138.4", "125.4", "116.2", "118.4", 1
data "What is 216.7 - 117.4?", "117.6", "104.3", "99.3", "89.3", 3
HistoryQuestions:
data 2
data "Who is the second president of the United States?", "Theodore Rosevelt", "John Adams", "George Washington", "Andrew Jackson", 2
data "What was the first war in the United States?", "The War of 1812", "The Indian American War", "The Revolutionary War", "World War II", 3
First, create a label to help identify what this data is for. The first data command I use to tell me how many questions will be read. Then each line contains 6 different values: the question, the 4 possible choices, and the answer. The answer is an integer number between 1 and 4 which will represent the user selecting A,B,C,or D.
Now look at the code to see how we read this data into the array using a FOR loop.
restore MathQuestions
read size
dim math(size) as TestQuestion
for i = 1 to size
read math(i).question
read math(i).choice1
read math(i).choice2
read math(i).choice3
read math(i).choice4
read math(i).answer
next i
restore HistoryQuestions
read size
dim history(size) as TestQuestion
for i = 1 to size
read history(i).question
read history(i).choice1
read history(i).choice2
read history(i).choice3
read history(i).choice4
read history(i).answer
next i
Whenever you call
read, it'll read the next DATA value. An internal pointer keeps track of which data is next. To reset that pointer, we use a command called
restore.
Restore MathQuestion will put that pointer at the beginning of the MathQuestion label. Technically, since it's all sequential, you can leave out the
restore HistoryQuestions in this example because the data falls immediately after the data for MathQuestion. However, it's good practice to use restore anyway, and it's safer.
We start by reading the first value, which is the number of questions. Then using that value we can define our array with the correct size. Now create the FOR loop to loop that same number of times as the size. Remember there are 6 values on each data line which represent a single question. So to read the entire line we call READ 6 times per loop for each question. READ take a single parameter, the variable it's going to read the data into. So for this example, it will be the various components of the UDT in each array element.
Main Loop:
Here's where we make the biggest change to your code's organization. The flow is much easier to understand and trace.
do
cls
if mode = MODE_TITLE then gosub Title
if mode = MODE_INSTRUCTIONS then gosub Instructions
if mode = MODE_SUBJECT_SELECT then gosub SubjectSelect
if mode = MODE_MATH_TEST then gosub MathTest
if mode = MODE_HISTORY_TEST then gosub HistoryTest
if inkey$() = "" then kFlag = 0
rem Display timer only during the test
if startTest = TRUE and finishedTest = FALSE
elapsedTime = (timer() - startingTime) / 1000
timeLeft = gameTime - elapsedTime
Text 20, 20, "Seconds Left: "+str$(timeLeft)
endif
loop
The series of IF statements you see will control which sub routine gets called. We need only to change the value of
mode to jump to a new section.
Use these constants for better organization. Constants are like variables, except their values cannot be changed once set. And, as a general rule of thumb, constants are typically written in all uppercase.
#CONSTANT MODE_TITLE = 1
#CONSTANT MODE_INSTRUCTIONS = 2
#CONSTANT MODE_SUBJECT_SELECT = 3
#CONSTANT MODE_MATH_TEST = 4
#CONSTANT MODE_HISTORY_TEST = 5
The actual values can be anything you want, as long as they're different. But rather than having your code look like this:
if mode = 1 then gosub Title
if mode = 2 then gosub Instructions
if mode = 3 then gosub SubjectSelect
if mode = 4 then gosub MathTest
if mode = 5 then gosub HistoryTest
You can use the constant names instead. The reason for this is because it's much easier to understand what each statement is doing. Plus, when you assign a value to mode, it's easier to remember which one does what. Does this enhance your program at all? No. But what it does is make life easier on the programmer.
Before, you had many loops to control key input. We're going to do away with all of that and use a flag instead. A flag has only two states, true or false; that is to say either the flag is raised up to signal something or the flag is down. Anytime the user presses a key, we raise the flag. We only accept user input when the flag is down. This prevents the same keystroke from being picked up by multiple
inkey$() calls. The flag is lowered again once it's detected all keys have been released, as seen in this part of the main loop:
if inkey$() = "" then kFlag = 0
We start with
mode defined as MODE_TITLE. This will then call the subroutine
Title.
Title:
set text size 30 : ink rgb (255,255,255),0
center text 320,60, "Welcome Science Mania!"
set text size 15
center text 320,160, "Press X if you don't want to read the instructions"
center text 320,180, "Press H if you want to read the instructions"
if inkey$() = "x" and kFlag = 0 then mode = MODE_SUBJECT_SELECT : kFlag = 1
if inkey$() = "h" and kFlag = 0 then mode = MODE_INSTRUCTIONS : kFlag = 1
RETURN
If the user presses
h then change the mode so the instructions subroutine is called instead. Otherwise, the other key will simply set the mode to call the test subjection selection code.
The Test:
Here's were we finally get to put those arrays in action. But first, let's look at the beginning of the MathTest subroutine. If the test hasn't started yet, display the ready message to the user and await for keyboard input. Once the user has pressed the spacebar,
startTest becomes true and we make a quick call to
ResetTest.
if startTest = FALSE
ink rgb(255,255,255),0 : set text size 30
center text 320,200, "Get Ready!"
set text size 15
center text 320,260, "Press the space bar to go start"
center text 320,280, "Press B to go back"
if inkey$() = "b" and kFlag = 0 then mode = MODE_TITLE : kFlag = 1
if Spacekey() = 1 and kFlag = 0
startTest = TRUE
kFlag = 1
gosub ResetTest
endif
else
What ResetTest does is reset a few important variables for us. These need to be reset any time a test is started or restarted. First, it starts us at question 1. Next, is the amount of time in the game. Then we grab the current time stamp which will be used for the countdown timer as seen in the main loop. And finally, make sure the test is not finished yet.
ResetTest:
currentQuestion = 1
gameTime = 15
startingTime = timer()
finishedTest = FALSE
RETURN
Now that the test has started, we move to the next section of code, which is contained inside the ELSE portion leftover from the snippet above.
else
if finishedTest = FALSE
ink rgb(255,255,255),0 : set text size 30
center text 320,200, math(currentQuestion).question
set text size 15
text 240,280, "A.) " + math(currentQuestion).choice1
text 240,300, "B.) " + math(currentQuestion).choice2
text 240,320, "C.) " + math(currentQuestion).choice3
text 240,340, "D.) " + math(currentQuestion).choice4
answer = asc(lower$(inkey$())) - 96
if answer > 0 and answer < 5 and kFlag = 0
kFlag = 1
if answer = math(currentQuestion).answer
inc gameTime, 5
else
dec gameTime, 5
endif
inc currentQuestion
if currentQuestion > array count(math())
finishedTest = TRUE
elapsedTime = (timer() - startingTime) / 1000
remainingTime = gameTime - elapsedTime
endif
endif
else
ink rgb(255,255,255),0 : set text size 30
center text 320,200, "Test Results"
ink rgb(0,255,0),0 : set text size 20
center text 320,280, "Congratulations! You finished the Math test with "+str$(remainingTime)+" seconds left."
ink rgb(255,255,0),0
center text 320,340, "Press any key to continue."
if inkey$() <> "" and kFlag = 0 then mode = MODE_SUBJECT_SELECT : kFlag = 1
endif
endif
First, we check if the test has finished. If not, then display the question with the possible choices. Now to get the user's selected answer.
answer = asc(lower$(inkey$())) - 96
Let's break this down. The user can select A, B, C, or D as the answer, but we marked the question answers with numbers 1-4. So how does this translate a letter into a number? First, get the user's input with inkey$(). Next, call lower$() as a safety measure so that both uppercase and lowercase letters can be accepted. ASC will return a character's
ASCII value. A lowercase 'a' has a value of 97, b is 98, c is 99, and so on. Finally, we subtract 96 from that ascii value and we'll end up with 1 for A, 2 for B, and so on. Now all you have to do is check to see if the user selected the correct answer or not, but first make sure the user entered a valid selection.
if answer > 0 and answer < 5 and kFlag = 0
This will ensure that the answer is only checked if the user selected one of the 4 available choices and prevents any other keyboard input from triggering a response.
If the answer is correct, add more time. Otherwise, subtract from the remaining game time.
if answer = math(currentQuestion).answer
inc gameTime, 5
else
dec gameTime, 5
endif
Move on to the next question by simply incrementing the value of
currentQuestion. If the value becomes larger than the size of the array containing our questions, then we've run out of questions. Mark the test as finished (finishedTest = TRUE) and record the time to track how much time remained after completion.
inc currentQuestion
if currentQuestion > array count(math())
finishedTest = TRUE
elapsedTime = (timer() - startingTime) / 1000
remainingTime = gameTime - elapsedTime
endif
The test is finished, show a stats/congratulatory page and wait for user input before moving on back to the main menu.
else
ink rgb(255,255,255),0 : set text size 30
center text 320,200, "Test Results"
ink rgb(0,255,0),0 : set text size 20
center text 320,280, "Congratulations! You finished the Math test with "+str$(remainingTime)+" seconds left."
ink rgb(255,255,0),0
center text 320,340, "Press any key to continue."
if inkey$() <> "" and kFlag = 0 then mode = MODE_SUBJECT_SELECT : kFlag = 1
endif
And here is the complete code. Comments are limited within the code because the documentation is all here in this thread explaining it all.
#CONSTANT TRUE = 1
#CONSTANT FALSE = 0
#CONSTANT MODE_TITLE = 1
#CONSTANT MODE_INSTRUCTIONS = 2
#CONSTANT MODE_SUBJECT_SELECT = 3
#CONSTANT MODE_MATH_TEST = 4
#CONSTANT MODE_HISTORY_TEST = 5
Type TestQuestion
question as string
choice1 as string
choice2 as string
choice3 as string
choice4 as string
answer as integer
EndType
rem read the math questions data into the math array
restore MathQuestions
read size
dim math(size) as TestQuestion
for i = 1 to size
read math(i).question
read math(i).choice1
read math(i).choice2
read math(i).choice3
read math(i).choice4
read math(i).answer
next i
rem read the history questions data into the history array
restore HistoryQuestions
read size
dim history(size) as TestQuestion
for i = 1 to size
read history(i).question
read history(i).choice1
read history(i).choice2
read history(i).choice3
read history(i).choice4
read history(i).answer
next i
set text font "arial"
rem set default mode to display opening title
mode = MODE_TITLE
do
cls
if mode = MODE_TITLE then gosub Title
if mode = MODE_INSTRUCTIONS then gosub Instructions
if mode = MODE_SUBJECT_SELECT then gosub SubjectSelect
if mode = MODE_MATH_TEST then gosub MathTest
if mode = MODE_HISTORY_TEST then gosub HistoryTest
if inkey$() = "" then kFlag = 0
rem Display timer only during the test
if startTest = TRUE and finishedTest = FALSE
elapsedTime = (timer() - startingTime) / 1000
timeLeft = gameTime - elapsedTime
Text 20, 20, "Seconds Left: "+str$(timeLeft)
endif
loop
Title:
set text size 30 : ink rgb (255,255,255),0
center text 320,60, "Welcome Science Mania!"
set text size 15
center text 320,160, "Press X if you don't want to read the instructions"
center text 320,180, "Press H if you want to read the instructions"
if inkey$() = "x" and kFlag = 0 then mode = MODE_SUBJECT_SELECT : kFlag = 1
if inkey$() = "h" and kFlag = 0 then mode = MODE_INSTRUCTIONS : kFlag = 1
RETURN
Instructions:
ink rgb (0,255,0),0 : set text size 30
center text 320,60, "Instructions"
ink rgb (255,255,255),0
set text size 15
Center text 320,160, "Welcome to the Science Mania Test Prep! In this Game"
center text 320,180, "you have to answer as many problems as you can"
center text 320,200, "before the timer runs out."
center text 320,220, "Good Luck!"
ink rgb (0,255,0),0
center text 320,420, "Press SPACE to start"
center text 320,400, "Press B to go back"
if inkey$() = "b" and kFlag = 0 then mode = MODE_TITLE : kFlag = 1
if spacekey() = 1 and kFlag = 0 then mode = MODE_SUBJECT_SELECT : kFlag = 1
RETURN
SubjectSelect:
ink rgb (255,255,255),0 : set text size 30
center text 320,200,"Which subject do you want to select?"
set text size 15
text 240,280,"1.)Math"
text 240,300,"2.)History"
text 240,320,"3.)Science"
center text 320,380, "Press B to go back"
startTest = FALSE
if inkey$() = "1" and kFlag = 0 then mode = MODE_MATH_TEST : kFlag = 1
if inkey$() = "2" and kFlag = 0 then mode = MODE_HISTORY_TEST : kFlag = 1
if inkey$() = "b" and kFlag = 0 then mode = MODE_TITLE : kFlag = 1
RETURN
MathTest:
rem Display get ready message until user has pressed space bar
if startTest = FALSE
ink rgb(255,255,255),0 : set text size 30
center text 320,200, "Get Ready!"
set text size 15
center text 320,260, "Press the space bar to go start"
center text 320,280, "Press B to go back"
if inkey$() = "b" and kFlag = 0 then mode = MODE_TITLE : kFlag = 1
rem user pressed space bar to start the test
if Spacekey() = 1 and kFlag = 0
startTest = TRUE
kFlag = 1
rem reset variables needed for test
gosub ResetTest
endif
else : `Test has started
rem if test is not finished (test is still running)
if finishedTest = FALSE
rem Display test question and possible choices
ink rgb(255,255,255),0 : set text size 30
center text 320,200, math(currentQuestion).question
set text size 15
text 240,280, "A.) " + math(currentQuestion).choice1
text 240,300, "B.) " + math(currentQuestion).choice2
text 240,320, "C.) " + math(currentQuestion).choice3
text 240,340, "D.) " + math(currentQuestion).choice4
rem Read user input
answer = asc(lower$(inkey$())) - 96
rem If user selected a valid choice
if answer > 0 and answer < 5 and kFlag = 0
kFlag = 1
rem if user selected correct answer
if answer = math(currentQuestion).answer
inc gameTime, 5
else : `User selected incorrect answer
dec gameTime, 5
endif
rem move onto the next question
inc currentQuestion
rem if no more questions in this test, mark test as finished
if currentQuestion > array count(math())
finishedTest = TRUE
elapsedTime = (timer() - startingTime) / 1000
remainingTime = gameTime - elapsedTime
endif
endif
else : `Test is finished
ink rgb(255,255,255),0 : set text size 30
center text 320,200, "Test Results"
ink rgb(0,255,0),0 : set text size 20
center text 320,280, "Congratulations! You finished the Math test with "+str$(remainingTime)+" seconds left."
ink rgb(255,255,0),0
center text 320,340, "Press any key to continue."
if inkey$() <> "" and kFlag = 0 then mode = MODE_SUBJECT_SELECT : kFlag = 1
endif
endif
RETURN
HistoryTest:
rem Display get ready message until user has pressed space bar
if startTest = FALSE
ink rgb(255,255,255),0 : set text size 30
center text 320,200, "Get Ready!"
set text size 15
center text 320,260, "Press the space bar to go start"
center text 320,280, "Press B to go back"
if inkey$() = "b" and kFlag = 0 then mode = MODE_TITLE : kFlag = 1
rem user pressed space bar to start the test
if Spacekey() = 1 and kFlag = 0
startTest = TRUE
kFlag = 1
rem reset variables needed for test
gosub ResetTest
endif
else : `Test has started
rem if test is not finished (test is still running)
if finishedTest = FALSE
rem Display test question and possible choices
ink rgb(255,255,255),0 : set text size 30
center text 320,200, history(currentQuestion).question
set text size 15
text 240,280, "A.) " + history(currentQuestion).choice1
text 240,300, "B.) " + history(currentQuestion).choice2
text 240,320, "C.) " + history(currentQuestion).choice3
text 240,340, "D.) " + history(currentQuestion).choice4
rem Read user input
answer = asc(lower$(inkey$())) - 96
rem If user selected a valid choice
if answer > 0 and answer < 5 and kFlag = 0
kFlag = 1
rem if user selected correct answer
if answer = history(currentQuestion).answer
inc gameTime, 5
else : `User selected incorrect answer
dec gameTime, 5
endif
rem move onto the next question
inc currentQuestion
rem if no more questions in this test, mark test as finished
if currentQuestion > array count(history())
finishedTest = TRUE
elapsedTime = (timer() - startingTime) / 1000
remainingTime = gameTime - elapsedTime
endif
endif
else : `Test is finished
ink rgb(255,255,255),0 : set text size 30
center text 320,200, "Test Results"
ink rgb(0,255,0),0 : set text size 20
center text 320,280, "Congratulations! You finished the History test with "+str$(remainingTime)+" seconds left."
ink rgb(255,255,0),0
center text 320,340, "Press any key to continue."
if inkey$() <> "" and kFlag = 0 then mode = MODE_SUBJECT_SELECT : kFlag = 1
endif
endif
RETURN
ResetTest:
rem start test with question 1
currentQuestion = 1
rem start test with 15 seconds
gameTime = 15
rem record when test started
startingTime = timer()
rem mark test as unfinished
finishedTest = FALSE
RETURN
MathQuestions:
data 4
data "What is 2+2?", "2", "1", "3", "4", 4
data "What is 99+1?", "50", "60", "100", "98", 3
data "What is 56.1 + 82.3?", "138.4", "125.4", "116.2", "118.4", 1
data "What is 216.7 - 117.4?", "117.6", "104.3", "99.3", "89.3", 3
HistoryQuestions:
data 2
data "Who is the second president of the United States?", "Theodore Rosevelt", "John Adams", "George Washington", "Andrew Jackson", 2
data "What was the first war in the United States?", "The War of 1812", "The Indian American War", "The Revolutionary War", "World War II", 3