Project: A Simple Javascript Reaction Game

Screen 2

Screen 2 consists of two <div> elements, screen2a and screen2b.

When players click on the "Submit Names" button in screen1, they'll be directed to the screen on the left below.  When they click on "Start Game", the screen changes to the screen on the right.

Javascript Game screen2a screen2b

The upper section of the screens above is screen2a. The lower section is screen2b.

Javascript Game screen2ascreen2a consists of 4 red boxes that display information about the target to click on, the best time achieved, reaction time for the current round and average time for the current player. Average time is displayed only at the end of 10 rounds for each player.

Javascript Game screen2b
screen2b is where the shapes will be drawn. It consists of 11 <div> elements – answer, shape1, shape2, shape3 and so on till shape10.

answer is a special <div> element that is displayed as either a "Start Game" button, a "Click Here To Continue" button, or a shape, depending on the context of the game at that moment.

To get this part of the game to work, we need to code a total of 12 functions.

The main function to code is the createGame(players, roundsPerPlayer) function. Let’s do it now.

createGame(players, roundsPerPlayer)

This function does two main things. The first is to hide screen1 and display screen2a and screen2b. The second is to add an event handler for answer.

answer is a special <div> element that serves two roles: Before a game starts and after it ends for each player, answer will be displayed as a button. During a game, it’ll be displayed as the target shape that users are required to click on.

For instance, suppose there are two players and for the sake of simplicity, let’s say each player gets to play three rounds. Here’s how answer will be displayed as:

Button – Shape – Shape – Shape – Button – Shape – Shape – Shape – Button

In the actual game, each player gets to play ten rounds. Hence, if there are five players, answer will be displayed as a button 6 times and as a shape 50 times.

Clear? Let’s start coding the function now.

First, let’s declare the function as follows:

function createGame(players, roundsPerPlayer) {

}

Next, within the curly braces, add three lines of code to hide screen1 and display screen2a and screen2b. Try doing this yourself.

After that, we want to display answer as a button that says "Start Game". To do that, we’ll use a function that we’ll be coding later. This function is called showAsButton() and takes in one argument – the message to display on the button. To use this function, we simply write

showAsButton("Start Game");

Add that to the createGame() function.

Next, let’s declare and initialize some variables as shown below:

var timesClicked = 1, oneSet = roundsPerPlayer + 1,
    createdTime, reactionTime, totalTime = 0, bestTime = 0,
    numberOfPlayers = players.length,
    playerNumber = 0,
    status = "button";

We'll be using each variable as we go along.

We are now ready to code an event handler for answer.

First, let's assign an empty anonymous function to the onclick property of answer. Try doing this yourself.

Within the anonymous function, we have the following switch statement:

switch (status) {
    case "button":

    case "shape":

}

Before we code this switch statement, we need to first understand how to do some calculations.

Suppose we have two players and each player gets to play three rounds.

The table below shows how timesClicked is linked to other variables. timesClicked is a variable (declared earlier) that we use to keep track of the number of times the answer element is clicked. numberOfPlayers and oneSet are both variables declared earlier.

In this example:

numberOfPlayers = 2
oneSet = 4

oneSet equals 4 because Button – Shape – Shape – Shape is considered a set in this example.

Javascript Game Calculations

To understand the table above, recall that the % operator gives us the remainder when the first number is divided by the second. Hence, 4%4 gives 0. Similarly, 8%4 also gives 0.

As you can see from the last column above, when timesClicked%oneSet equals zero (the 4th and 8th row), the game ends for each player.

In addition, when timesClicked equals oneSet*numberOfPlayer + 1 (last row, 9===4*2+1), the entire game ends.

Clear?

With that, we are ready to code the "button" case for the switch statement.


Warning: This switch statement is relatively complex and involves a lot of nested code. Be very careful when following the instructions below and ensure that you insert your code into the correct code block.


First, let's start with the if-else statement below:

if (timesClicked === oneSet*numberOfPlayers + 1) {

} else {

}

Copy this if-else statement into the "button" case of the switch statement. You are advised to indent it neatly so that you can clearly see that this if-else statement belongs to the "button" case.

Done?

Let's code the if-else statement now.

The if case occurs when all players have finished their rounds. When that happens, the game proceeds to the next screen to display their results.

To do that, we need to call a function called displayResults(). This function takes in the variable players as argument. Try calling this function yourself. We’ll be coding the function later.

Next, let's move on to the else block. For the else block, we need to do the following:

  1. Set the variables totalTime, reactionTime and bestTime to 0.
  2. Update the innerHTML of besttime, reactiontime and averagetime to 0, 0, and "-" respectively.
  3. Call the function removeAllShapes().
  4. Call the function createAllShapes() and assign the result to createdTime.
  5. Change the className property of answer to "shape".
  6. Change the value of status to "shape".

Try doing these yourself. Do not worry about the removeAllShapes() and createAllShapes() functions, we’ll be coding them later.

With that, our if-else statement is complete. Just add a break statement (outside the if-else statement) and the "button" case is complete.

Now, we are ready to code the "shape" case.

Within the"shape" case, we need to calculate the values for reactionTime, totalTime and bestTime.

To do that, we use the code below:

reactionTime = (Date.now() - createdTime) / 1000;
totalTime = totalTime + reactionTime;
if (reactionTime < bestTime || bestTime === 0) {
    bestTime = reactionTime;
}

The formula for totalTime and bestTime should be self-explanatory. The formula for reactionTime requires some explanation. As we are currently coding the event handler for answer, the line

reactionTime = (Date.now() – createdTime) / 1000;

will be executed only when the user clicks on answer. Hence, we use Date.now() to get the time of clicking. We then subtract createTime from it and divide the result by 1000 to change the time to seconds (the time was originally in milliseconds). createdTime is the time when answer was created; we got it from the createAllShape() function in the "button" case above.

Clear? Let’s move on.

After calculating the values for the three variables, we need to update the innerHTML of besttime and reactiontime to the new values of bestTime and reactionTime respectively. Try doing that yourself.

Next, add the following if-else statement to the"shape" case:

if (timesClicked%oneSet === 0) {

} else {

}

The if block is executed at the end of the game for each player. Within the if block, we need to first call the setTime() method. This method calculates and stores the values for bestTime and averageTime for each player. We coded this method earlier when coding the storePlayers() function.

To use the method, we write

players[playerNumber].setTime(bestTime, totalTime, roundsPerPlayer);

As the players array stores the Player objects that represent each player, we access the current player by writing players[playerNumber].

playerNumber is a variable (declared earlier) that keeps track of the current player.

We then use this object to call the setTime() method and pass in the values for bestTime, totalTime and roundsPerPlayer.

Clear? Let's move on.

Now we need to update the innerHTML of averagetime to the averageTime property of the current player.

Next, we also need to update the innerHTML of instructions to "End of Game". Try doing these yourself.

Finally, we need to increment playerNumber by one, change the value of status to 'button', call the removeAllShapes() function, and change answer to a button using the showAsButton() method. We pass in "Click Here To Continue" to the showAsButton() function. Try doing these yourself.

With that, the if block is complete. The else block is relatively easy. Within the else block, we only need to call the removeAllShapes() followed by the createAllShapes() function. We assign the result of the createAllShapes() function to the createdTime variable. Try doing these yourself.

Once you are done, the "shape" case is complete. Just remember to add a break statement for this case and the switch statement is done.

Once you exit the switch statement, you need to increment timesClicked by one and the event handler is complete. With that, our createGame() function is also complete.

If all goes well, you should see the following when you refresh your browser:

You can check out the sample answer for createGame() here.

showAsButton(message)

Now, let’s move on to code those functions that we used in the createGame() function earlier.

The first function we used is the showAsButton(message) function. This function has one parameter – message – and is used it to display answer as a button.

We need to do three things inside the function:

  1. Change the className property of answer to "instructionbutton".
  2. Set the style.display property of answer to "block".
  3. Set the innerHTML property of answer to message.

Try declaring and coding this function yourself. Once you are done, you should get the following:

You can check out the sample answer for showAsButton() here.

removeAllShapes()

Next, let's code the removeAllShapes() function. As the name suggests, this function removes all shapes from screen2b.

Recall that screen2b consists of 11 <div> elements – answer, shape1, shape2, shape3 and so on till shape10?

In order to remove all the shapes from screen2b, we need to set the style.display property of all the 11 elements to "none".

Try doing this yourself.

Hint: You can use a for loop to remove shape1 to shape10.

Done? If you refresh your browser now, you will notice that nothing much has changed.

The only difference is instead of getting a

"Uncaught ReferenceError: removeAllShapes is not defined"

error, you get a

"Uncaught ReferenceError: createAllShapes is not defined"

error.

You can check out the sample answer for removeAllShapes() here.

createAllShapes()

Now, let’s move on to code the createAllShapes() function. This function is responsible for drawing the shapes in screen2b.

As mentioned above, screen2b consists of 11 <div> elements – answer, shape1, shape2, shape3 and so on till shape10.

answer is the target shape that users are required to click on while the other 10 elements are randomly drawn shapes to distract the user. Let’s call them distractors.

screen2b will be divided into 10 sections and the 10 distractors will be drawn within each section.

However, not all distractors will be drawn, only about 70% of them will be drawn on each round.

Out of these 10 distractors, one of them will be randomly chosen to be replaced by the target shape (answer).

answer will always be drawn.

To ensure that answer is unique, the colors of the distractors are restricted so that none of them share the same color as answer.

Clear? An example is shown below. In this example, answer replaces shape8.

Javascript Game answer example

Do not worry if you don’t fully understand the logic behind this game at the moment. It should get clearer once we finish coding this function.

Let’s do that now.

The createAllShapes() function uses a number of other functions that we’ll be coding later. These functions are the getColor(), getShape(), getShapeWidth(), getShapeHeight(), getShapeLeft(), getShapeTop(), drawOneShape() functions.

First, let's declare our createAllShapes() function:

function createAllShapes() {

}

Within the curly braces, we also need to declare the following variables:

var i, createdTime, answerIndex, answerColor, width, height, top, left, shape, color, idName, maxWidth, maxHeight;

Let's start with the maxWidth and maxHeight variables first. These two variables store the maximum width and height of each shape.

As mentioned above, screen2b is divided into 10 sections. Specifically, it divided based on the layout below:

Javascript Project screen2b

Hence, to get the maximum width of each shape, we simply need to figure out the width of screen2b and divide that by 5 (or multiply that by 0.2). To get the width of screen2b, we can use the clientWidth property. This property gives us the inner width of an element in pixels. The statement below shows how we can calculate the maximum width of each shape and assign it to maxWidth:

maxWidth = document.getElementById("screen2b").clientWidth * 0.2;

Add the line above to the function.

Next, to get the maximum height of each shape, we can use the clientHeight property and divide that by 2 (or multiply that by 0.5). Try modifying the formula above to get the maximum height of each shape and assign it to the variable maxHeight.

After getting the values of maxWidth and maxHeight, we need to randomly generate an integer from 1 to 10 and assign it to answerIndex. This is where the answer element will be drawn.

To do that, we use the code below:

answerIndex = Math.floor(Math.random() * 10) + 1;

Math.random() randomly generates a number from 0 to 1 (non-inclusive). When we multiply that by 10, we get a number from 0 to 10 (non-inclusive). Next, we use Math.floor() to round the number down to the nearest whole number. This gives us an integer from 0 to 9. Finally, we add one to get an integer from 1 to 10.

After getting the position of answer, we need to get its color. To do that, we call the getColor() function and assign the result to answerColor. Try doing this yourself.

Now, we are ready to create the shapes. To do that, we use the for loop below.

for (i = 1; i <= 10; i = i + 1) { 
    if (Math.random() > 0.3 || i === (answerIndex)) {
        //draw the shape        
    }
}

Within the for loop, we have an if statement.

As you can see from the condition of the if statement, a shape is drawn only when Math.random() gives us a number greater than 0.3 or when i equals answerIndex.

Let's code the if block now. This block is quite long, so do be careful.

Within the if-block, we need to do four things:

    1. Call the getShape() function and assign the result to shape.
    2. Call the getShapeWidth() and getShapeHeight() functions and assign the results to width and height respectively. When calling the functions, we need to pass in the arguments maxWidth and maxHeight respectively.
    3. Use an inner if statement to check if shape is "Circle" or "Square". If it is, we use the following code to change the values of height and width.
      height = Math.min(width, height);
      width = height;
    4. Use an inner if-else statement to draw the actual shapes.

Try coding steps 1 to 3 yourself.

Done?

Now, we are ready to code step 4.

We’ll use the inner if-else statement below:

if (i === answerIndex) {

} else {

}

The content below discusses the code of this inner if-else statement.

Within the if block of this inner if-else statement, we first call the getShapeLeft() function and pass in the values of answerIndex, maxWidth and width. We then assign the result to left.

Next, we call the getShapeTop() function and pass in the values of answerIndex, maxHeight and height. We assign the result to top.

Finally, we call the drawOneShape() function and pass in "answer", shape, answerColor, height, width, top and left as arguments.

This function is responsible for drawing a single shape at the given position (based on top and left).

After drawing the shape, we update the innerHTML of instructions and answer using the code below:

document.getElementById("instructions").innerHTML = answerColor + "<BR>" + shape;
document.getElementById("answer").innerHTML = "";

Last but not least, we assign the current time to createdTime. Try doing the above yourself.

With that, the inner if block is complete. The inner else block is easier. Within this else block, we’ll be drawing a distractor.

To do that, we need to call getShapeLeft() and pass in i, maxWidth and width as arguments. We assign this result to left. Next, we call getShapeTop(), pass in i, maxHeight and height, and assign the result to top.

After that, we need to get the color of the distractor using the getColor() method and assign the result to color. However, we need to ensure that color is different from answerColor. To do that, we can use a do-while loop to repeatedly call the getColor() method until we get a color that is different from answerColor. Try doing this yourself.

After getting the color, we are almost ready to use the drawOneShape() function to draw the distractors. However, we need to pass in the id of the <div> element that we are drawing as the first argument. Recall that the ids of the distractors are given by shape1, shape2 etc? For instance, when i = 3, the id is shape3.

Try forming the id yourself and assign it to the variable idName.

After that, you can call drawOneShape() to draw the distractor using the code below:

drawOneShape(idName, shape, color, height, width, top, left);

With that, the inner else block is complete. You can now exit the inner else block, the outer if block and the outer for loop. After that, simply return createdTime and the function is complete.

If you run the code now, you'll see that nothing much has changed.

The only difference is instead of getting a

"Uncaught ReferenceError: createAllShapes is not defined"

error, you get a

"Uncaught ReferenceError: getColor is not defined"

error.

You can check out the sample answer for createAllShapes() here.

getShape(), getColor()

Next, let’s code the getShape() and getColor() functions. These functions are for generating the shape and color of the <div> elements.

We'll start with the getShape() function.

Within the getShape() function, we need to first declare an array called shapes with the following values: "Square", "Circle", "Rect" and "Oval". Next, we need to use the Math.random() and Math.floor() functions to randomly generate a number from 0 to 3 and assign the value to a variable called i. Finally, we need to use the statement below to return the element at index i:

return shapes[i];

Try coding this function yourself.

Next, let's move on to the getColor() function.

This function is similar to the getShape() function. It contains an array called colors as shown below:

var colors = ["Green", "Blue", "Red", "Orange", "Pink", "Yellow", "Purple", "Black", "Gray", "Cyan"];

We’ll randomly generate a number from 0 to 9 and use that to return a color from the array above. Try coding this function yourself.

Once you are done, you can refresh your browser to run the code. Once again, nothing much has changed other than the error message. This time, it should inform you that the getShapeWidth() function is not defined.

You can check out the sample answer for getShape() and getColor() here.

getShapeWidth(maxWidth), getShapeHeight(maxHeight)

Let's code the getShapeWidth() and getShapeHeight() functions now. These two functions contain formulas for randomly generating the width and height of the shapes and have parameters maxWidth and maxHeight respectively.

To generate the width and height of each shape, we use the formulas

Math.max(20, Math.floor(Math.random() * (maxWidth + 1)))

and

Math.max(20, Math.floor(Math.random() * (maxHeight + 1)))

respectively.

For the first formula,

Math.floor(Math.random() * (maxWidth + 1))

randomly generates an integer from 0 to maxWidth. However, we do not want the width to be smaller than 20. Hence, we use the Math.max() function to select the bigger of two numbers – the randomly generated number, or the number 20. If the randomly generated number is smaller than 20, the value 20 will be selected, hence ensuring that the minimum width is 20.

We’ll skip the explanations for the second formula as it is very similar to the first. You can refer to Chapter 6.4.1 for a recap of the Math.max() function.

Try writing these two functions yourself (one for generating width and one for height).

Once you are done, you can refresh the browser to run the code. You'll get an error message that says getShapeLeft() is not defined.

You can check out the sample answer for getShapeWidth() and getShapeHeight() here.

getShapeLeft(index, maxWidth, width), getShapeTop(index, maxHeight, height)

Let's code the getShapeLeft() and getShapeTop() functions now. These two functions are for calculating the position of each shape.

getShapeLeft() takes in three arguments – index, maxWidth and width – and calculates the horizontal position of the shape. Specifically, it calculates the number of pixels that it should be from the left edge of screen2b. index refers to the index number of the <div> element. For instance, shape3 has an index of 3.

Within the function, we first check if the index of the shape is more than 5. If it is, we subtract 5 from the index. The reason for doing this is because any shape with an index more than 5 is on the second row. As we are calculating how far left a shape should be, we do not need to differentiate whether the shape is on the top or bottom row. Try writing the if statement yourself to adjust the index of the shape as described above.

Next, we calculate how far left the shape should be using the formula below:

Math.floor(Math.random() * (maxWidth - width + 1)) + ((index - 1) * maxWidth)

We’ll skip the explanation of this formula as it is purely mathematical in nature. Try writing this function yourself to return the result of the formula.

Next, let’s move on to getShapeTop(). This function takes in three arguments – index, maxHeight and height – and calculates the vertical position of the shape. Specifically, it calculates the number of pixels that it should be from the top edge of screen2b.

This function uses an if-else statement. If index is less than or equal to 5, the formula is given by

Math.floor(Math.random() * (maxHeight - height + 1))

If index is more than 5, the formula is given by

Math.floor(Math.random() * (maxHeight - height + 1)) + maxHeight

Try writing this function yourself.

Once you are done, you can refresh your browser to run the code. You'll get an error message that says drawOneShape() is not defined.

You can check out the sample answer for getShapeLeft() and getShapeTop() here.

drawOneShape()

Let's define the drawOneShape() function now.

This function has 7 parameters – idName, shape, color, height, width, top and left. Try declaring the function yourself.

Within the function, we’ll use an if-else statement to draw the different shapes. The different shapes are achieved by varying the style.borderRadius property of each <div> element. For instance, if the shape is a circle, we use

document.getElementById(idName).style.borderRadius = 0.5 * width + "px";

to set the borderRadius property of the element.

Try modifying the pseudocode below to set the borderRadius property of each element based on the formulas given:

if (shape is "Circle") {
    //Border radius = 0.5 * width + "px"
} else if (shape is "Oval") {
    //Border radius = 0.5 * width + "px / " + 0.5 * height + "px"
} else {
    //Border radius = 0
}

After setting the borderRadius property, we need to set other CSS styles for the element.

Specifically, we need to set the style.top, style.left, style.width and style.height properties of the element to the top, leftwidth and height parameters respectively. In addition, we need to append the string "px" to each of the parameters (similar to what we did for the borderRadius). Try doing this yourself.

Finally, we need to set the style.backgroundColor and style.display properties to color and "block" respectively. Try doing this yourself.

With that, the drawOneShape() function is complete.

If you refresh your browser now, you should get the following:

You can check out the sample answer for drawOneShape() here.