You can see an index of all the posts in this series: go to index.
If you want to review my progress, here are the files from the end of the previous post: capturing user input with events
At the end of the previous post I left myself with a few challenges. The first of these was to add two more questions, a single answer type and a multiple answer type and to make one of these a picture question.
To do this is simply a matter of adding the data for the question to the end of my JSON file. I want to add the following question:
Which of the following buildings are over half a kilometre tall?
A: One World Trade Centre
B: Empire State Building
C: Burj Khalifa
D: Shanghai Tower
E: Petronas towers
The correct answers are A: One world trade centre C: Burj Khalifa and D: Shanghai Tower
In my quiz.json file, this would look like this:
}, { "type": "multi", "questionText": "Which of the following buildings are over half a kilometre tall?", "image": null, "options": [ { "optionText": "One World Trade Centre", "correct": true }, { "optionText": "Empire State Building", "correct": false }, { "optionText": "Burj Khalifa", "correct": true }, { "optionText": "Shanghai Tower", "correct": true }, { "optionText": "Petronas Towers", "correct": false } ] } ]
Note, line 82 is the closing brace from the end of the previous question, but I’ve added a comma after it. Line 110 is the existing closing square bracket – my new question must be within this. One thing for me to bear in mind when I am adding new questions is to take care to check I have set the “correct” properties correctly for each option. For example, if I have a single answer type question with more than one option set as correct, the user will never be able to answer the question correctly or proceed further in the quiz.
The JSON data format is flexible enough that I don’t have to add questions at the end, I can insert new questions anywhere in the sequence. I want to add a new picture question, shown below, as the second question in the quiz.
How many squares can you find in this picture?
A) 14
B) 15
C) 16
D) 17
E) 18
The correct answer is D) 17.
The first thing to do with an image question is to add the image file to my images folder, as I’ll need to add the filename to the JSON data. In this case the file is called html-squares-question.png. So between the question 1 and the current question 2 data, I add the following:
{ "type": "single", "questionText": "How many squares can you find in this picture?", "image": "html-squares-question.png", "options": [ { "optionText": "14", "correct": false }, { "optionText": "15", "correct": false }, { "optionText": "16", "correct": false }, { "optionText": "17", "correct": true }, { "optionText": "18", "correct": false } ] },
Having made those changes, I opened the page using Web Server for Chrome and saw this:
Not good, it was almost certainly because I’d introduced an error in my JSON file. I confirmed this by opening the developer tools and looking at the console. This is what I saw:
These sort of errors are easy to make, and it’s usually something like a missing comma or quote marks or curly brace or bracket. Unfortunately JSON errors are shown by character position, so they can be difficult to track down. If I’m having trouble finding the problem, I use a search engine to search for JSON lint or JSON validator and i can find a range of online tools that allow me to paste my JSON in and which will then highlight any errors.
Once I had everything working correctly, I could see that the questions appeared in the order I added them in the JSON file. Because the solution has been built with this flexibility in mind, the question numbering automatically adapts to the new questions and the introductory text and final question text and button still correctly appear on the first and last questions. I hope you can see how much easier this is than creating completely new pages for each new question, which was what I had in the first version of the quiz.
Now on to the next challenge which was to limit the user to two attempts at each question.
The first thing I need to do here is adjust the html so the “Try again…” part of the incorrect feedback can be hidden independently of the rest of that feedback. I can do that by using a span element. I changed that line in index.html so it looks like this:
<p class="hidden" id="incorrect"><strong>Sorry! That's not the right answer!</strong> <span id="retry">Try again...</span></p>
Now I need to add some code to quiz.js to handle this new behaviour. First, I added a couple of new properties to the quiz object, just before the initialiseQuiz function:
maxAttempts: 2, attemptNum: 0,
These hold the maximum attempts allowed and the current attempt number respectively. Even for values that I expect to stay constant, such as the maximum number of attempts, it’s better to have these as a property or variable I can easily change. If I later decide that two attempts is too harsh, for example, and I want to change it to three then I only have to change that number in this one place.
Next I made some changes to the checkAnswer function:
checkAnswer: function() { // increment attempt number quiz.attemptNum++; let hadPermittedAttempts = (quiz.attemptNum == quiz.maxAttempts); // Hide incorrect feedback in case it is still showing from a previous attempt quiz.setVisibility(quiz.page.incorrect, false); // Check users selections to see if they have selected the correct options let options = quiz.quizData[quiz.currentQuestion].options; let answeredCorrectly = true; for (let i = 0; i < options.length; i++) { let isSelected = document.querySelector('#option' + i).classList.contains('selected'); if ((options[i].correct && !isSelected) || (!options[i].correct && isSelected)) { answeredCorrectly = false; } } if (answeredCorrectly || hadPermittedAttempts) { let optButtons = document.querySelectorAll('.optionButton'); for (let i = 0; i < options.length; i++) { optButtons[i].disabled = true; } quiz.setVisibility(quiz.page.submit, false); let atFinalQuestion = quiz.currentQuestion == quiz.quizData.length - 1; quiz.setVisibility(quiz.page.finalQuestion, atFinalQuestion); quiz.page.nextQuestionNum.innerHTML = quiz.currentQuestion + 2; quiz.setVisibility(quiz.page.nextQuestion, !atFinalQuestion); } if (answeredCorrectly) { quiz.setVisibility(quiz.page.correct, true); } else { quiz.setVisibility(quiz.page.incorrect, true); quiz.setVisibility(quiz.page.retry, !hadPermittedAttempts); } },
At the top of the function, on lines 79-80 I now increment the number of attempts and check to see if the user has reached the maximum number of attempts.
The options will now be disabled and the submit button hidden both when the question is answered correctly or when the maximum number of attempts have been reached. So the condition on line 96 has changed to reflect this.
Also notice that I’ve removed the line to display the correct feedback. This is now in its own if statement, starting on line 109.
Finally, when I show the incorrect feedback, I also need to either show or hide the “try again” part based on whether the user has more attempts or not, which I do on line 113. Notice that I’m the logical ! operator here, because I only want to show this part of the feedback when the user has not used all their permitted attempts.
When I open the quiz in the browser now, I see that, if I answer incorrectly on a second attempt, I get the incorrect feedback (without the “Try again…” part), but the question is also disabled and I get the next question or replay quiz button:
The final part of the challenge was to add scoring. I want to award the player 0 points for an incorrectly answered question, 1 point for a question answered correctly on the second attempt and 2 points for a question answered correctly on the first attempt. To achieve this, I first need to add a few parts to the index.html file:
<p class="hidden" id="correct"><strong>That's correct!</strong> <span id="nextQuestionPrompt" class="hidden">Now try the next question...</span></p> <p class="hidden" id="incorrect"><strong>Sorry! That's not the right answer!</strong> <span id="retry">Try again...</span></p> <p class="hidden" id="score">You scored <span id="points"></span> point<span id="pluralPoints">s</span> for this question.</P> <button class="button hidden" id="nextQuestion">See question <span id="nextQuestionNum"></span></button> <div class="hidden" id="finalQuestion"> <p>Thanks for playing the quiz!</p> <p>Your final score is <span id="totalPoints"></span> out of <span id="maxPoints"></span> points.</p> <button class="button" id="replay">Play again</button> </div>
On line 21, I’ve added a new paragraph element to show the score for each question. Note this uses an empty span element to hold the points.
On line 25 I’ve added a further paragraph element to show the final score at the end of the quiz. Again this uses empty span elements to hold the total score and the maximum number of points.
In quiz.js, I need to add a few more properties (I added these just above the initialiseQuiz function):
maxPointsPerQuestion: 2, penaltyPerExtraAttempt: 1, totalScore: 0,
The maxPointsPerQuestion property is the score that will be applied for a correct answer on the first attempt. The penaltyPerExtraAttempt property is the number of points that will be deducted for each subsequent attempt. Finally, totalScore will be used to keep track of the player’s cumulative score.
Because I’ve added some new elements to my page, I need to adjust the updatePage function to initially hide the score:
// Hide the score this.setVisibility(this.page.score, false);
Now I need to make several changes to the checkAnswer function:
if (answeredCorrectly || hadPermittedAttempts) { let optButtons = document.querySelectorAll('.optionButton'); for (let i = 0; i < options.length; i++) { optButtons[i].disabled = true; } quiz.setVisibility(quiz.page.submit, false); let points = answeredCorrectly ? quiz.maxPointsPerQuestion - ((quiz.attemptNum - 1) * quiz.penaltyPerExtraAttempt) : 0; quiz.totalScore += points; quiz.page.points.innerHTML = points; quiz.setVisibility(quiz.page.score, true); quiz.setVisibility(quiz.page.pluralPoints, points != 1); if (quiz.currentQuestion == quiz.quizData.length - 1) { quiz.page.totalPoints.innerHTML = quiz.totalScore; quiz.page.maxPoints.innerHTML = quiz.quizData.length * quiz.maxPointsPerQuestion; quiz.setVisibility(quiz.page.finalQuestion, true); } else { quiz.page.nextQuestionNum.innerHTML = quiz.currentQuestion + 2; quiz.setVisibility(quiz.page.nextQuestion, true); } }
If the question has been answered correctly or the player has used up their permitted attempts then I need to calculate the score for the question. I do this on line 110 using a ternary operator. If the question has been answered correctly, the score is calculated by taking the maximum points per question and subtracting the number of extra attempts multiplied by the penalty score. If the question has been answered incorrectly, then it scores zero.
I then add the score to the running total on line 111 and display the score on lines 112-114.
The next part, on lines 116 – 123 is restructured to allow for showing the final score if the user is on the final question.
The final change I need to make, is to the showNextQuestion function:
showNextQuestion: function(reset) { // Reset attempt number quiz.attemptNum = 0; // Remove existing answer options while (quiz.page.options.firstChild) { quiz.page.options.removeChild(quiz.page.options.firstChild); } // Show next question (or first if resetting) if (reset) { quiz.currentQuestion = 0; quiz.totalScore = 0; } else { quiz.currentQuestion++; } quiz.updatePage(); },
I’ve restructured the code here so that I can also reset the score if the user is restarting the quiz. This happens on line 167.
When I save these changes and refresh the page, I see I now get a score at the end of each question:
When I get to the final page, I’m shown the final score:
That’s it for my quiz game, although it’s now an easy matter to add further questions if I want to build out the prototype into a full quiz.
You’ll find the final files here: html quiz or you can view the GitHub repository. You can also play a live version.
In the next part of this series, I’ll start work on a new game – a version of the classic pen and paper game, Tic Tac Toe (otherwise known as Noughts and Crosses). This will involve more complex graphics than I’ve used so far, managing a game state that involves turn taking and winning, losing and drawing conditions, and creating a game artificial intelligence to provide a computer opponent.
Be First to Comment