React Beyond Basics
Color Match

The Color Match Game

Enough with the boring examples. Let’s build something fun.

Since this is the first game we will tackle, I picked a very simple one. I named it Color Match.

The game UI will present the player with two randomly selected colors. For example, green and red. The first color is always rendered with black ink while the second color is rendered with a third random color value. The challenge is for the player to determine if the meaning of the first color word matches the ink color of the second word. This is not as easy as it sounds.

Here’s a screenshot of the final game:

picture7

The bottom word RED in the screenshot above is rendered with a green ink color. The top word GREEN matches the ink color of the word RED. The player should click YES to win this round.

On every YES/NO click, the UI should indicate whether the click is correct or not and then start another color match challenge.

You can play all the games that we will build in this book at jscomplete.com/react-games

It is important that you understand each game’s complete logic before you start with it. Take some time to familiarize yourself with each game before you follow along with how to build them.

The focus of this game is on updating the state of a class component and styling things based on values from the state. I am going to build the entire game in a single Game component to keep a focus on just the state-updates concept. We will also need to use a timer to delay the reset of the game after each button click so that the players get to see if their previous click was correct or not.

Let’s go through this game one step at a time. The key strategy is to find small increments and focus on them rather than getting overwhelmed with the whole picture. Don’t shoot for perfection from the first round. Have something that works and then iterate, improve, and optimize.

While building all the games in this book, I will sometimes implement things in not-very-ideal ways first, show you why they are not ideal, and then go through the process of replacing them with better versions. Give yourself credit if you can spot me doing something like that.

Step 1 – Initial Markup and Style

I like to start with some mock markup and any styles that I can come up with for that markup. With simple games like this one, this is usually an easy task. Just put mock static content where the dynamic content will eventually be.

The UI of this game is super simple. We can put two static color words where the dynamic ones will be. We can build the whole markup before we even start doing any React code. However, it is always good to build your static markup with the React components that make sense for the app logic.

I’ve decided to use a single Game component for this game. This decision will allow us to focus on mastering the skill of updating a component state without worrying about data flow. Don’t worry though, games #2, #3, and #4 will all have multiple components and plenty of data flow.

This book is about React, so to keep the examples short and focused on just React, I’ll give you a starting template for each game. The template will have a static mock version of the DOM and all the CSS that you need to style the game the way I did. Feel free to customize the CSS as you wish. I am not a CSS expert and I will probably do some questionable things in my styles.

I opted to do many features of the games, and especially this first one, through CSS styles. I love the simple power of CSS and I think that if something can be done adequately with CSS, you should consider that. For an example of that, I will use only CSS to indicate whether a YES/NO button click was correct or not. In the React component, we just need to use the right CSS className. This game will also have an example of dynamically styling an element with an inline JavaScript object. I made some of these decisions so that you can learn how to work with both ways of dynamically styling elements in your React UIs.

The game’s starting HTML/CSS template jsdrops.com/rg-4.1

Here’s the template HTML that you can start with:

class Game extends React.Component {
  render() {
    return (
      <div className="game">
        <div className="help">
          Does the meaning of the top word match the ink
          color of the bottom word?
        </div>
        <div className="body">
          <div className="game-status status-playing" />
          <div className="meaning">*Meaning*</div>
          <div className="ink">*Ink*</div>
          <div className="buttons">
            <button>YES</button>
            <button>NO</button>
          </div>
        </div>
      </div>
    );
  }
}

ReactDOM.render(<Game />, mountNode);

The bolded words above are the mock color word values that will eventually need to be generated at random.

As you can see, I am giving most elements a CSS className with the intention of doing most of the styling with pure CSS. Here’s all the CSS that I used to style the game:

.game { max-width: 600px; margin: 0 auto; }
.body {
  text-align: center; position: relative;
  background-color: #efefef; border: thin solid #ddd;
}
.meaning, .ink {
  font-size: 350%; font-weight: bold; margin: 10px;
}
.help { color: #666; margin: 10px; text-align: center; }
.buttons button {
  margin: 20px; font-size: 200%; padding: 10px 20px;
}
button:focus { outline: none; }
.game-status {
  position: absolute; width: 100%; height: 100%;
  opacity: 0.85;  background-color: #eee;
  font-size: 125px; font-weight: bold;
}
.status-playing {
  display: none;
}
.status-correct:after { content: "\2713"; color: green; }
.status-wrong:after { content: "\2717"; color: red; }

Here’s how the UI should look like when executing the HTML/CSS above:

picture8

Note how I have 2 bolded classes in the templates' CSS. These are the special classes that will cause the UI to indicate a correct or wrong answer. You can test them out by changing the "status-playing" class in the mock HTML to status-correct and status-wrong.

When using status-correct, the UI should show:

picture9

When using status-wrong, the UI should show:

picture10

Change the class back to status-playing to continue. This is the starting status of the game.

Step 2 – Picking Random Colors

We need to render three random colors (one for the meaning word, one for the ink word, and one for the actual color of the ink word).

To keep the game simple, let’s just use the primary colors of black, blue, red, green, and yellow. We can put these values in an array and just sample the array to get a random color.

const colors = [
  'black', 'blue', 'red', 'green', 'yellow'
];

// To get a random color, use _.sample(colors)

Although picking a random value from an array can be easily done with vanilla JavaScript, it is not the focus of this book. Whenever possible, I’ll use methods from the lodash library to keep us focused on just the React API. The above example uses the sample method from lodash. It’s a method that will return a random value from an array.

Now all we need to do is place three random value somewhere in variables so that we can use them. First, we have to come up with good names for these three variables, then, we need to decide if we need to place them on the state of the Game component.

The names I came up for these three colors are meaningWord, inkWord, and inkColor. Naming is hard, but I think these are pretty clear.

Since every time a player clicks YES or NO we have to reset the game and start a new one, those values will have to be changed between games. If we place all of the three values on the state of the Game component, then changing them will reset the game:

// In the Game class

state = {
  meaningWord: _.sample(colors),
  inkWord: _.sample(colors),
  inkColor: _.sample(colors),
};

We can now use these three values in the rendered output of a Game component. The first two are easy. They just replace the mock values that we have in the template. The inkColor is a special one. It has to drive the style of the inkWord. For that, we can use React’s special style property to dynamically set the color of the ink word.

Here are the three changes that we need for the color div elements in the markup:

<div className="meaning">
  {this.state.meaningWord.toUpperCase()}
</div>

<div
  className="ink"
  style={{ color: this.state.inkColor }}
>
  {this.state.inkWord.toUpperCase()}
</div>

Since CSS accepts named colors, we can use the color words directly in the style object.

The game should now render two random color words and style the second one with a third random color value.

Playground link for the code so far

Step 3 – Planning UI Statuses

Think about the different UI statuses that this game could be in at any moment:

  • If the player has not clicked a button yet, the game would be in laying mode.

  • If the user clicks on a wrong answer, the game would be in a status to indicate that.

  • If the user clicks on a correct answer, the game would be in a status to indicate that.

Let’s officially name these three UI statuses playing, correct, and wrong and collectively refer to them as gameStatus.

Do we need to manage this gameStatus on the Game state object?

To answer any question like that, you need to answer the following question first:

Do we need React to re-render the Game component if the value of this gameStatus changes?

Yes, we do. This probably means that this new gameStatus belongs to the state of the Game component. It should start as playing.

Since we already have CSS classes designed to change the UI for the correct/wrong statuses, all we need to do in the component’s render method is use the value of gameStatus from the state object. We initialize this value on the state first:

// In the Game component

state = {
  meaningWord: _.sample(colors),
  inkWord: _.sample(colors),
  inkColor: _.sample(colors),
  gameStatus: 'playing', // Possible other values: correct, wrong
};

Then, in the render output, we use it

<div className={
  `game-status status-${this.state.gameStatus}`
}/>

You can test these UI statuses by testing all three possible values on the initial gameStatus state.

Playground link for the code so far

Step 4 – Implementing User Interactions

Once the UI is prepared to honor all the values that we planned and initialized on a component’s state, implementing users' interactions will mostly be about updating that component’s state. This usually also involves some application-level logic.

For the Color Match game, on every button click:

  • We need to finish the game. This means changing the gameStatus field to either correct or wrong.

  • We need to compute whether the game has a meaning/ink match or not. This is as simple as comparing meaningWord with inkColor. Let’s name this new field meaningInkMatch.

  • Since meaningInkMatch can be true or false and the player can click either yes or no, we have 4 possibilities about whether the click was correct or wrong. Let’s assume we have a Boolean flag for the button clicked as well. Let’s name this one yesClick. If meaningInkMatch and yesClick are both true or both false, that indicates a correct click. If they have different Boolean values, that indicates a wrong click.

The logic description above has many conditional branches. Think about a way to avoid having to use many if statements for this logic.

The logic in the last point above can be computed with a logical XNOR gate, which is the logical compliment of the XOR gate. See the chart below for the four possibilities of the XNOR logical operator:

picture1

JavaScript does not have a logic XNOR operator, but we can use the JavaScript Bitwise XOR (^) operators to implement it.

In JavaScript, A ^ B returns a 1 in each bit position for which the corresponding bits of either but not both operands are 1s. Since false is 0 and true is 1, the XNOR operation that we need to implement the logic in the last point above can just be (meaningInk ^ yesClick) === 0.

Here are the code modifications that we need to implement all of the logic identified above:

// Implement the clicking logic. In the Game class
handleClick = (yesClick) => {
  this.setState((prevState) => {
    if (prevState.gameStatus !== 'playing') {
      return null; // Do nothing.
    }
    const meaningInkMatch =
      prevState.meaningWord === prevState.inkColor;
    const correct = (meaningInkMatch ^ yesClick) === 0;
    return { gameStatus: correct ? 'correct' : 'wrong' };
  });
};

// Register the click handlers on buttons
<div className="buttons">
  <button onClick={() => this.handleClick(true)}>
    YES
  </button>
  <button onClick={() => this.handleClick(false)}>
    NO
  </button>
</div>

Note a couple of things about the code above:

  • We had to wrap the handleClick function with an inline function before we passed it as the onClick reference function because we needed to invoke it with different arguments for each button. Keep in mind that this will create new inline functions every time the Game component gets re-rendered. This is okay for infrequent renders that do not affect children components, which is the case we have here. Keep it simple.

  • I used an updater function for setState because I needed to access values that are on the state of the Game component to compute the new state. This might not be necessary because within one game run the colors values do not actually change.

Note how I added an edge-case check to handleClick to disable clicking on the buttons unless the game is in playing mode. Returning null from the setState updater function makes the setState call do nothing.

Have you identified a problem in the design of the game so far? If the colors values do not change in a single game, why do we have them on the state?

The golden React rule about this is: Only place on a component state what is absolutely needed to be on the state. Minimize state elements and use computations (and caching) otherwise.

If you think about the Game state object, there is some value overlap among what we have so far on the state. Changing either gameStatus or any of the color values will trigger the UI to render a new game session. We can minimize the state here.

However, before we refactor anything we wrote so far to minimize the game’s state object, let’s finish the minimum requirements of the game. We need to reset the game after every click. Let’s make that our next step.

Playground link for the code so far

Did you notice how most of this very important past step was not about React but rather about coming up with the JavaScript logic needed for the user interaction? I think 80% of your power as a React developer comes from your JavaScript skills.

Step 5 – Resetting the Game

This one is straight forward. After every click, we need to reset the state object to the same object we used for the initial state. Let’s use a browser timer to give the players a bit of delay so that they can see the result of their last click. After experimenting with a few values for this timer, I settled on having that delay be half a second.

Here are the modifications that we need to implement this step:

handleClick = (yesClick) => {
  this.setState((prevState) => {
    if (prevState.gameStatus !== 'playing') {
      return null; // Do nothing.
    }
    const meaningInkMatch =
      prevState.meaningWord === prevState.inkColor;
    const correct = (meaningInkMatch ^ yesClick) === 0;
    return { gameStatus: correct ? 'correct' : 'wrong' };
  }, this.resetGameAfterDelay);
};

resetGameAfterDelay = () => {
  setTimeout(() => {
    this.setState({
      meaningWord: _.sample(colors),
      inkWord: _.sample(colors),
      inkColor: _.sample(colors),
      gameStatus: 'playing',
    });
  }, 500);
};

Since we need to trigger the timer after the first setState call is completed (and remember setState is to be considered asynchronous), I used the second argument of setState (inside handleClick). This is the first time we are seeing this, so let me explain!

The second argument to setState is a callback (function reference). React guarantees it will execute the function referenced as the second argument after the setState operation is done. This is similar to the way Node.js functions work. The resetGameAfterDelay function will be executed after the correct/wrong UI update is done.

Inside resetGameAfterDelay, we just use a regular browser timer to update the state of the component once again (after 500 milliseconds). However, this time the state will be reset to its initial shape.

This change makes the minimum requirements of this game complete! Go ahead and play a few rounds.

Playground link for the code so far

You probably noticed that I just copied the initial state object above, which is considered a code smell (not following the DRY principle). I am aware of that, but I don’t want to optimize yet because I am still not sure that we need all these elements on the state. When possible, delay thinking about optimization until after you validate your design decisions.

Step 6 – Refactoring the State

Although changing the three color values should change the UI, if you carefully analyze the code you’ll see that this operation happens only when we change the gameStatus back to playing (in the resetGameAfterDelay function). That gameStatus change by itself will trigger React to re-render the UI. The three colors never change between clicks. We can simply store them as instance properties. Furthermore, the meaningInkMatch computation does not have to be in the click handler. That too is a constant value between clicks.

For this simple example, these optimizations will not make a big difference, but I want you to start getting into the habit of questioning what you put on the state. Don’t use the state as a dumping ground for anything your app needs.

Since we need these three colors when the game is first mounted and every time we click the buttons, let’s create a function that returns an object to represent them and place that object as an instance property in the Game component. We can also include the meaningInkMatch value as part of that object. I’ll name this instance object colorValues.

The function that generates the three random colors does not have to be part of the Game component at all. Let’s call in randomColors:

// Top-level function
const randomColors = () => {
  const meaningWord = _.sample(colors);
  const inkWord = _.sample(colors);
  const inkColor = _.sample(colors);

  return {
    meaningWord,
    inkWord,
    inkColor,
    meaningInkMatch: meaningWord === inkColor,
  };
};

Both the colors array and this new randomColors function can be extracted into their own module and tested separately. This is important because it simplifies the Game component and reduces its dependencies. For example, if we do this extraction, the game stops directly depending on lodash (for the sample method). It would only depend on a colors module. We can change the colors module without needing to change the Game component.

To use the randomColors function:

// The new Game state is now:
state = {
  gameStatus: 'playing',
};

// Use an instance property for the color values
colorValues = randomColors();

This way, when the component is first mounted, it will get an object with 3 random colors and 1 Boolean flag.

We need to change the Game component in a few places to use the colorValues instance property:

// Change handleClick to use this.colorValues
handleClick = (yesClick) => {
  this.setState(prevState => {
    if (prevState.gameStatus !== 'playing') {
      return null; // Do nothing.
    }
    const correctClick =
      (this.colorValues.*meaningInkMatch ^ yesClick) === 0;
      return {
        gameStatus: correctClick ? 'correct' : 'wrong',
      };
    },
    this.resetGameAfterDelay
  );
};

// Change resetGame to use reset this.colorValues
resetGameAfterDelay = () => {
  setTimeout(() => {
    *this.colorValues = randomColors();
    this.setState({ gameStatus: 'playing' });
  }, 500);
};

We will also need to change the render function to use the color values from the new instant property rather than from the state. Try to make that change on your own. The playground link below has all the changes we need, including the render function.

Playground link for the code so far

Step 7 – Getting Feedback and Improving

This step is not really about React, but before you label something done or MVP, stop testing it with your biased eyes and put it in-front of beta testers. Their fresh feedback is very valuable.

For example, you might have noticed that the game draws a lot more color-ink mismatches than it does matches. A beta tester would certainly notice that if you did not. Beta testing this game made me tune the UI and the help message as well.

Let’s fix this mismatch bios. We need the game to be a match at least 50% of the time. This is a simple change. We can use the Math.random() < 0.5 condition for that and for 50% of the game sessions we can force the colors to be a match.

Since the other 50% of the sessions will also randomly pick a match, let’s use a lower value for the match-forcing part. I think a 40%/60% split would be good.

Here is a possible implementation:

const randomColors = () => {
  const meaningWord = _.sample(colors);
  const inkWord = _.sample(colors);
  const inkColor =
    Math.random() < 0.4 ? meaningWord : _.sample(colors);

  return {
    meaningWord,
    inkWord,
    inkColor,
    meaningInkMatch: meaningWord === inkColor,
  };
};

Now test the game some more. There should be a good balance between match sessions and mismatch sessions. Make sure you iterate on users' feedback. I think we can now label this progress as MVP. #ShipIt.

Playground link for the final code of this game jsdrops.com/rg-4.7

Bonus Challenges

Maintain a score property for all played games. Give the player 1 point for each correct click and take 1 point for each wrong click. Display the current score somewhere in the game UI.

Note this new score property needs to be kept between game sessions. Where do you think you can store it?

Let me make the challenge a little bit interesting: You can’t use a global variable for this new score value.

If you feel like tackling a harder challenge, make this game a lot harder by swapping top/bottom logic. For every new game, change the help message to swap the top and bottom words:

  • First game help message: Does the meaning of the top word match the ink color of the bottom word?

  • Second game help message: Does the meaning of the bottom word match the ink color of the top word?

If your testers think this game is easy even after this change, maybe make the top/bottom swapping at random!