Building a Complete Game

Pro Members Only

This lesson is available if you have an active subscription.

Alternatively, some member might be able to gift it to you.

If you already have a subscription, you can sign in.

Building a Complete Game

Subscription Required

You must have an active subscription to access this content.
If you already have a subscription, you can sign in.

Objective

In this lesson we build a complete tic tac toe game in React ⚛

Project Files

We start with an empty project we've been using for all the lessons in the course.

By the end of the lesson we have a full game. Here is the final result:

App.tsx
AppState.tsx
Layout.tsx
Status.tsx
Board.tsx
Square.tsx
Log.tsx
import { useGameState } from './AppState';
import { Column, Row } from './Layout';
import { Status } from './Status';
import { Log } from './Log';
import { Board } from './Board';

export default function App() {
const {
boardValue,
boardStatus,
handleSquareClick,
handleStepClick,
boardHistory,
} = useGameState();
return (
<Row gap={20}>
<Column gap={20}>
<Status boardStatus={boardStatus}/>
<Board boardValue={boardValue} onSquareClick={handleSquareClick} />
</Column>
<Log boardHistory={boardHistory} onStepClick={handleStepClick} />
</Row>
);
}
javascript
typescript
react
playwright

Enjoy free content straight from your inbox 💌

No spam, unsubscribe at any time.

Transcript

00:00

In this tutorial, we will build a Tacto application and this is going to be a great demonstration of how you can build complete apps using React. So let's go. Let's first take a look at the user interface that we are trying to build. Most user interfaces can be broken down into a collection of row and columns. For example, the root of our application can be thought of as a row consisting of two items. On the left we have the game status and the game board, and on the right we have a history of all the moves that have been made in this game. And then the first item within this row can be further thought of as a column by itself consisting

00:34

of the game status followed by the game board. And from here, if we dig even deeper, we can break down the game board into a collection of columns and rows as well. We can think of it as a column consisting of three rows where each row in itself consists of three squares. One more thing that we want these rows and columns to support is the ability to create gap between the individual children. Creating these rows and column components is extremely easy in modern web design thanks to CSS Flex Box. Now that we know how we are going to tackle the design,

01:06

let's build some basically our components that we are going to use to build our game. Both the row and the column component are going to support a children pop and of course a gap prop which decides how much space is going to exist between the individual children. Now because these components are going to be used again and again, it's a good idea to try to optimize them and we're going to do a minor thing of wrapping it up with reactor memo to make these as pure components. Underneath both of these components are pretty simple. They are simple divs with CSS flex box one with a direction row and the other with a direction column.

01:40

And with the CSS cap property, whenever we create components, it's a good idea to test them out. So within our app component, we create a very basic layout layout that consists of a single column consisting of two rows. The first row has alpha one, beta one and gamma one, and the second row has similar T with different gap values in the column and the individual rows. And if you jump to the browser, it does look valid. We have a single column consisting of two rows with a nice big gap in the column and a small gap in the first row and a bigger gap in the second row. A good approach to follow is to take a top-down approach

02:14

of building the core user interface portions of our game as separate react components. The top components for this particular tic-TAC two application seem to be the game status on the left-hand side, a game bo under that status component and a log of all of the moves on the right hand side. With these components created, let's put them into a nice layout. We create a single row consisting of two children. The first child is a column consisting of the game status and the game board and the second child is the log component. And with this we have the root level setup

02:46

of a game all done and ready to go with the overall placeholders for the key portions of the game created and wide into the layout, let's flesh them out to be full fledged stateless components that work on top of past in props. It's a good idea to think about the kinds of data that are flying around in our application. Fortunately thanks to TypeScript, we can actually document these TE types as well. For example, for our player, we either expected to be an X or an O. The individual squares within the board can be x XO or null. If no player has placed anything in there yet,

03:20

and our board consists of nine squares and therefore we can represent it as a simple array of square values. A particular value of the board can result in a few statuses. For example, one of the players might be a winner if they have their values in a straight line. Similarly, if no one is a winner and all of the squares have been occupied, then the game is in a draw state and if you don't have a winner and it is not a draw, then the next player needs to go. Now these types are enough to specify the kind of state for logic that we would need in a single tic-tac toe game. Now if you want to track the history of the game,

03:56

we would need to store all of the different board values through the different moves, and we can do that with a board history type that is an array of board values. I think these are all the data types that we will need in the stick T game. So let's jump into the kind of props that we would need for the root level components. First up, we have the game status component and that's going to be pretty easy because we can render it completely based on the board status value. The board itself is going to be a bit more complicated because we first need to figure out what kind of props we would use for the individual squares. Fortunately, the squares themselves are quite easy.

04:31

Each square has a square value and we want to handle the user clicking on the square. Once we have the square defined, the board sort of reveals its props to us automatically. It consists of a board value, which as we defined previously is just an array of squares and an on square click, which will bubble up the square that the user actually clicked. Finally, we have the game log component and for this we will pass in the board history as well as the utility function that allows the user to step to an individual point within that history with the types of the individual props defined,

05:04

let's jump into our main application component and create some dummy values that we can pass down to the components. We don't have to build any logic right now. We just want to make sure that we have the values that the components expect. We have a simple dummy board consisting of nine members, a very simple board status of type winner, a new op handle, square click, and a handle step click and a board history that can consists of the current value of the board. Just as an example, with these dummy values available to us, we can start enforcing the props for the component. First up, we expect the board status

05:36

for the status component and then we use that to either render the winner or draw or the next player. Of course, the app component is now going to complain, but we can fix that quite easily because we have the dummy board status available. Now that we have the status done, let's turn our attention to the next component, which is the main game board. But before we can work on the board, we need to work on the square and our square is going to be a pretty simple component. It is going to be a simple table with a fixed width and a height, a nice background, a slight water, a large font size with font weight bold

06:11

and it'll center its child using CSS flex box. With these styles defined, we just create the square accepting square props, returning a simple div wide to that style and the passed in on click rendering the square value. Now that we have the square defined, we can work on our board component. We expect the board props to be passed in and to make our life easier on working with the individual squares, we will create this utility function called create props that takes the index of a particular square and then returns the value that would be valid

06:43

for that particular square. And an on click function that when triggered, we'll call on square click for that square. With this utility function out of the way, we can render that layout that we talked about, which is a single column consisting of three rows where each row has those three individual squares for the boat. There will be no gap between the column children or the row children. And for the square we use our create props function to spread the props that we generate for the individual squares. With the board component uplifted,

07:15

let's fix the error within our application with the board expects to be passed in the correct props. And of course we have these dummy values available to us that we will just pass in. The final route level component that we have to uplift is the log component. And of course we will start that by expecting the log props to be passed in and we can actually just render out the board history as individual list items with a button applied to on step click and a display showing either go to start or go to the move number. And just like all of the other components,

07:46

once we have added these mandatory props, we have to pass them in. So let's jump into our app and pass in the dummy values that we have readily available for us to go. Now even though at this point our application is not going to be really functional, however, we do have a full layout with all of the components wide to the necessary props and then a rendering out the values that get passed in. A great thing about react hooks is that they allow us to separate game logic from game rendering. Now that we have the rendering down, we can work on the logic

08:17

and the brains of the application in isolation. So we jump into a new module called App State, bring in the required suspect from React, which is EU state and all of the types that we defined within the app module that will help us work with the game logic. Let's work through some utility functions. We create one called create board value, which basically creates an array consisting of nine items for us all filled with Nels, which will be our initial board state. In terms of determining the status of the board, we have a few utilities.

08:48

The first one is calculate winner, which takes a board value and then determines if we have a particular player that has won the game. There are only eight possible winning combinations within tacto, and if the values at these particular indexes are the same, that means that you have a line through that three by three board. So we just look through the winning combinations, get the three indexes A, B, and C, and check if position A has been occupied by a player and the values at B and C are equivalent to the value at a. And if that is indeed the case,

09:22

then we have the winner at position A. Otherwise, if none of the winning combinations have been occupied by any player, there is no winner and we can safely return now with the calculate winner utility out of the way. Another good utility that would be good to have is the calculate is full. This simply needs to make sure that all of the squares are not equal to null. And if that is indeed the case, then the board is indeed full. The final utility related to board status is calculate Next. This simply takes the current step within the game and then based on that determines if X or O should go next.

09:58

The game starts off at index zero where X should go, and then at index one O should go and index two X should go. And therefore we can see that it is a simple evens check. For even numbers, it is X and for odd numbers it is O. With these utility functions out of the way, let's think about how we would want to organize our game state with React. It's a good idea to keep related state into a single object, and our game state can be thought of as a game history and a single step within that history that represents the current board value. Once we have that board value, we will be able

10:31

to derive anything else that we want about our board. For example, if there's any winner, is there a draw on who's next? Now that we know what kind of data structure we are working with, let's create a hook that manages all of our game state for us. The first thing of course that we do within this hook is to call a new state to initialize a new game state. It starts off with an empty board at step zero. Now whatever the current state of the game state might be at a future point, we will be able to derive the board value by looking at the board history at the game state dot step. Using the game state step, we can determine

11:07

who should be the next player. And then using the board value, we can determine if there is already a winner or if the board is completely full With these values, we can create a final answer for the board status. If there is a winner, then we have a winner. If there is no winner and the board is full, then we have a draw. Otherwise, it is time for the next player to make a move. This should give us everything that we need in terms of the current display of the game. So let's write some utilities to handle the interactions with the game. The first interaction that we need to handle is a click on one of the squares.

11:40

We can do that with the handle square click function. And the first thing that we will do within this is to make sure that there is no winner, the board is not already full, or the place that the user has clicked is not already occupied by any player. If all of these conditions are false, only then should we update the game state. All updates to the game board should result in a trim of any potential future history. So the first step that we do is to make sure that we only take the game history up till the current step. Next, we take the current board value, and from that we will determine the updated board value.

12:15

That's going to be pretty easy as we simply clone the current one and for the square that the user has clicked, we simply place the player that was intended to go for the current step. Finally, we take this updated board value, push it into the history, and then update the game state. With the new game history and the step incremented by one, the other interaction that we are going to handle is going to be even simpler, and that is the user deciding to move to a different point in the history. And for that purpose, we simply update the step to the past in value. Finally, within our use game state hook,

12:47

we return everything that we have just looked at, which is the board value, the board status, the two handle click functions. Along with the board history, we have the brains of our application as a hook and the visuals of our application as react components. All that is left to do is to combine the two into a functioning game, plus jump into our app component and bring in the new de created use game state hook that we just wrote from the app state. And now we can just replace these dummy values that we have with the values that we can now get from the use game State hook.

13:19

This is a good demonstration of incremental software design where we build different things independently and eventually just wire them together. For example, the app and the app state can be worked on independently. If we follow the contract that we expect, we jump into the game and we are at our initial point, the next player to go is X. Now the X is smart, it decides to go in the center. This particular O is not that clever and doesn't decide to go in one of the corners. This mean that X has already won the game. If X places it in one of the corners,

13:52

then O has to complete that corner. Otherwise X would win immediately. And now X gets the opportunity to do a double threat. It is trying to win on a diagonal as well as on one of the sites. O can only protect in one of them and X will win by completing the other. You can see that we have the complete game history. The status has updated and X has won. Being the generous host that X is, it allows O to go back to the point before O made that initial mistake. This time O decides to go in one of the corners. Now there is no guarantee that either of them will win,

14:26

and if both plays don't make a mistake, it'll be a draw. For example, X can try and win by a diagonal O notices counters. Now O can win on a side, so X counters now X seems to be winning in the center, so O needs to counter, and now X makes one final attempt to win through the center O counters and X finishes with the draw between the two players. Hopefully this tutorial was a good demonstration of how you can break down a sufficiently complicated task into smaller chunks that can be tackled independently,

14:59

and that is a great skill to have when we are working in larger teams. As always, thank you for joining me and I will see you in the next one.