Code Tetris in 200 Lines of Code using React

  |   React
Tiny Tetris in React

When I learn a new programming language, one of my favourite things to do is write a simple Tetris game in it. I've written Tetris in C, C++ and Java, and most lately I've written Tetris using JavaScript and React. I was amazed when I realised it can be done in less than 200 lines of code when using React!

Writing Tetris is exciting, as it forces you to understand many aspects of a programming language / framework in a very small application. My tiny Tetris Application uses many React aspects (Components, Memo's, References, Hooks...) as well as JavaScript Intervals. If you want an example of how these different things can collaborate together in an app smaller than a To Do list, this is the perfect place.

This blog explains the different parts of the tiny Tetris & React game. Reading here you will learn the basic components of writing a simple Game.

The source code for the tiny Tetris game is on Github here and working in Stackblitz here. You can also Play the Game Hosted on GitPages.

But Why?

I needed to learn React Fast as I had to rewrite the rendering engine of AG Grid in React. AG Grid's React rendering engine is the second React application I wrote. The first is my tiny Tetris game. I found the React Tetris to be a wonderfully fulfilling way of learning some of the edges of React. Both AG Grid and a Tetris game are essentially Rows and Columns of cells.

AG Grid - Market Leading JavaScript Datagrid

For the Record - AG Grid is the first JavaScript application I have ever written! Now that's a different story, for now back to Tetris....

Source Code Overview

The game code is in the /src/ directory.

  • index.js renders the Game component in the root element created by create-react-app.
  • /src/board.js contains the main Board component used to render the Tetris game.
  • /src/shapeFactory.js contains the definition of the tetris shapes.
  • /src/useBoard.js contains the bulk of the code and is the Hook that controls the game with the main game logic.
  • /src/useInterval.js is a Hook to make working with setInterval easier for the game logic.

Games Use The MVC Pattern

All game programming uses a form of MVC, whether it be a 3D PlayStation blockbuster, or a 2D smartphone game.

In my tiny Tetris, the Model is called the Stage, synonymous to a Theatre Stage for a Play. The Stage contains all Model information regarding the state of the Tetris Game. The Stage has two parts

  1. the background which I've called the Scene and
  2. the currently moving Shape.

The Scene you can think of as the background to the game - it keeps track of what squares are empty and what squares are filled with blocks.

The currently moving Shape is what the player is controlling.

For rendering, the Stage merges the Scene and the Moving Shape together to create the Display. The Display is effectively a 2D array of what should get rendered on the screen, regardless of why (ie a square can be coloured Red either because it's part of the Moving Shape or the Scene (background), which reason doesn't matter to the rendering).

The resulting Display object, with details of what squares should be filled in, is then passed to the React UI Component for iterating over to create the DOM.

This is all controlled by the useBoard hook in useBoard.js

Game State Management in React

React's useState() hook is used to store all the State information in the Model. This is very helpful, for example the Display data is stored in React State, which then enables React to know the UI needs updating as the Display is mapped to React UI Components.

e.g. the state section from useBoard

export function useBoard() {

 const [scene, setScene] = useState(()=> createEmptyScene() );
 const [shape, setShape] = useState(()=> randomShape() );
 const [position, setPosition] = useState({x: 0, y: 0});
 const [display, setDisplay] = 
                       useState( ()=> 
                              mergeIntoStage(scene, shape, position) );
 const [score, setScore] = useState( 0);

Effects Control Updates

Cause and Effect is demonstrated beautifully. React's useEffect() hooks is used to update state items that depend on other state items.

For example useEffect(updateDisplay, [scene, shape, position]) is used so the logic to update the Display is called whenever the Scene, Moving Shape or the Position of the moving shape has changed.

    useEffect(updateDisplay, [scene, shape, position]);
    useEffect(removeFullLines, [scene]);

The Tick Controls Game Speed and Logic

Every game Ticks. A game Ticks for every animation frame. A Tick does two things:

  1. Update the Model (in our game, this includes moving the shape) and then
  2. Update the Display (in our game, we merge the Scene to create the Display, which React then Renders).

In our game we Tick every 600ms (the time it takes for the block to move down one row) as well as if the user hits a key to move the Moving Shape. These are the only events that can update the Scene.

We have wrapped the interval with a hook that you can find in useInterval.js

useInterval(tick, 600);

On updating the Model, the application will do the following:

  1. If a keyboard event caused the tick, the Moving Shape will attempt a Rotate or a Move Down, depending on what key was pressed. The move will succeed if the new position fits inside empty squares in the Scene. This includes keeping the Shape within the boundaries of the Scene.
  2. If the interval caused the Tick, the Moving Shape will be moved down. If the shape doesn't fit, then the move down is cancelled, the Shape is merged permanently into the Scene, and a new Shape is created and placed at the top of the Stage.
    function tick() {
        if (!movePosition(0, 1)) {
            placeShape();
        }
    }

    function placeShape() {
        setScene(mergeIntoStage(scene, shape, position));
        setShape(randomShape());
        setPosition({x: 0, y: 0});
    }

Shape Factory

The Shape Factory is responsible for generating Shapes. It selects randomly from the three basic shapes: Box, Line and L.

This code is found in /src/shapeFactory.js

And is a simple array of objects.

const SHAPES = [
    {
        shape: [
            {x:0, y:0},
            {x:0, y:1},
            {x:1, y:0},
            {x:1, y:1}
        ],
        width: 2,
        height: 2,
        rotate: false
    },
    ...
]

The random function uses the length of the array as the boundary so any new shape objects added into the array will automatically be used in the game.

export function randomShape() {
    return SHAPES[Math.floor(Math.random() * SHAPES.length)];
}

Something to Try: Try to create some more shapes, such as "T" or "+".

Set Interval in React

Setting Intervals in React is non trivial, as you have to be careful not to suffer from stale references. The topic is explained in other peoples blogs such as this one, so I won't duplicate the details here. However you will see in our game, a custom useInterval() hook was created to allow using JavaScript Intervals safely inside React.

import React, {useEffect, useRef} from 'react';

export function useInterval(callback, delay) {

    const callbackRef = useRef();

    useEffect( ()=> {
        callbackRef.current = callback;
    }, [callback]);

    useEffect( ()=> {
        const interval = setInterval(()=> callbackRef.current(), delay);
        return ()=> clearInterval(interval);
    }, [delay]);

}

Something to Try: Change the Interval and watch the game at a different speed, or how about have the interval decrease over time, so the game speeds up the longer you play. This can be done by changing the useInterval line in useBoard currently the game is hard coded to a tick of  600 milliseconds.

Game Display in React

The Display is the most trivialised (thanks to React and the DOM) of all the parts. React simple iterates over the Display using Row and Cell components, which then map to DIV items. All the rendering is then taken care of by the DOM and CSS. Any other stack I've used (eg C++) doesn't have the richness of React and the DOM to do the rendering, and as such would off taken a lot more coding in any other stack.

Something to Try: The Rendering could be swapped out for something else. Real games don't use React or the DOM, they use Canvas. If you want to learn how to write Canvas code, see if you can render the Scene using Canvas instead. This is controlled in the Board hook in board.js.

Use Memo to Limit DOM Updates

The Cells and Rows all use memo() for the components. This enables React to only update the DOM where changes have occurred, rather than update the entire display each time. This can be observed by using the React Dev Tools and observing where React updates the DOM between Ticks.

Something to Try: Try removing the memo() in board.js and observe how React will update everything each time the Display changes.

Colors

The current implementation uses Red blocks only.

Something to Try: Bring multiple colours into the game. Have the Shape Factory create Shapes using different colours (Red shapes, Blue shapes etc) and then have the Scene store block colour information for each block.

Summing Up

I learnt a lot from writing this Tetris Game, so I wanted to share it with the world. Happy Tetris'ing. One word of warning - if you do start writing Tetris, you will spend a lot of time playing (testing?) it!!

Testris Game source is on Github and the game is playable on GitPages.

Tetris in React

Read more posts about...