This post is a step by step guide to create a Tic Tac Toe Game With NativeScript. I’ll assume that you’re familiar with the basic concepts of NativeScript using Angular2. If it’s not the case, I recommend you to follow the official getting started guide, which includes the setup on every platform. Note also that the source code of this tutorial can be found on GitHub at this address.
First step, creating the application:
Creating the Domain
We’re going to start by creating the core domain of the application, based on POTOs (Plain old TypeScript Objects). The basic entities we will need are:
- The Board, containing multiple markable Squares. We will also define a size so it can dynamically rely on it for the creation of the Squares and the check of a potential win.
- The Squares, having a State indicating what has been played (Blank, Circle or Cross). They will also contain a set of coordinates to identify their position within the Board.
The main logic within the Board Entity will be the detection and handling of a victory. Let’s start by creating the Square which will be the single unit of the Board. We’ll put it into a new directory app/domain:
Note that we’re using an enum to define the SquareState. To make things easier for importing the entities later, let’s create an index.ts file where we will define all exports for the domain directory in one place:
We’re now ready to create the Board class. The first thing we need to do is to initialise the Squares to keep track of them. We’re going to use a simple, flat Array for this (instead of a multi-dimensional one). This will make the structure simpler for the Component afterwards:
It’s important to have a visualisation of the position of each square within the array. Consider the following picture:
Basically, the array is composed of a sequence of rows, separated by an of set which is equal to the size of the board.
Before going further with the implementation of the Board, let’s add its reference to the index.ts:
Implementing the Win Detection Algorithm
Let’s think about the algorithm that will be responsible to check for a win. Essentially, we’ll need to check if a newly marked Square triggered a win, and get the winning series of Squares in return. Since we’re using an array, the indexes will be enough. Before diving into the implementation, let’s explore all the different cases of a possible win. There is a win if any row, column, or diagonal is fully composed of Squares that are all equal, and aren’t Blank. Let’s take a look back at our Board representation and identify the related patterns.
Detecting a Win in a Row
For a win to be triggered in a row, we’ll need to check the sequence where the newly marked Square belong. The following picture shows the sequence that need to be checked after having marked the Square located at 1, 1:
Did you notice the pattern? In our case, the sequence that need to be checked is the one containing Squares with x = 1, and starts at index = (size of the board). If we generalise this, we know that we need to check the sequence having a size equal to the board’s size, starting at index = (Square.xPosition * board size). We then come to the following algorithm to gather the winning Square indexes in the marked Square’s row:
As we check through the indexes, we make sure that we return undefined as soon as we come across a State that is different that the given Square’s one, since it won’t be a win anyway.
Detecting a Win in a Column
The following picture shows the indexes that need to be checked in order to have a winning column match, when the marked Square is located at 0, 2:
This time, the sequences are not contiguous, but separated from each other by a number of indexes equal to the size of the board. We can also notice that the first element of each column will be the elements between index = 0 and index = (size of the board). Starting from the previous algorithm, we come to this one to check if there is a matching win in the given Square’s column:
Detecting a Win in the Diagonal
The difference between the diagonal and the previous cases, is that there is only one possible series. If we take the same approach as before and we look at the following picture:
We notice that the Square is in the diagonal only if x = y. Then we need to check the Squares separated by (board size + 1) indexes (and staring at index = 0). In our algorithm, that means that offset will be initialised with 0, and be augmented with (board size + 1) after every increment of the loop. Let’s introduce a new function that will handle the detection logic, as it’s common to every case:
Which allows us to write the following algorithm, for the Diagonal check:
Now we can also use the new function for the two previous cases:
Detecting a Win in Anti-Diagonal
Consider the following representation for the anti-diagonal:
In this case, we’re going to perform with the check only if (x + y) = (board size – 1). The initial offset will be (board size – 1), which will also be the increment. We then come up with the following implementation:
Let’s now create a wrap-up method to call all these functions. It will have to be called every time a player marks a Square, in order to get the winning indexes:
call function in order to provide a reference to
this to the child calls.
At this point, we can extract this logic into a new class in order to keep the Board nice and small. Let’s call it WinningIndexesRetriever and create it with the following content:
The WinningIndexesRetriever needs the Board size, as well as the Squares Array to perform, so we pass them to its constructor. Before going back to the Board, let’s add the reference of the WinningIndexesRetriever to the domain/index.ts file:
Implementing the Board
Now let’s define everything else we’ll need in the Board to serve the user interface:
- The information about the current State, to be able to know who’s playing next
- The scores of each States, to be able to keep track of multiple games
- a Property to indicate that there is a draw
- And of course, the possibility to mark a Square
Let’s start with the following implementation of the Board:
After having declared the needed instance variables, we initialise them in the constructor. We also introduce a method
startNewGame() which reset the
_isGameWon flag, initialise the Squares array and create a new instance of WinningIndexesRetriever. This method has been set public because we want to be able to call it explicitly from the user interface as well.
Let’s now implement a property indicating if the current game’s state is a draw. To do this, we’ll have to keep track of the marks that have been done. There is a draw when the marks count is equal to the number of Squares in the Board, and the game is not won:
Let’s now implement the method to mark a Square:
mark() method is responsible for transforming the Square state, as well as changing the Board’s current State. We also want to increment the marks count in order to notice a possible draw. Finally, we check if the game has been won by looking if we have some winning indexes after having marked the current Square. If it’s the case, we set the
_isGameWon flag, and we increment the winner’s score.
Before moving on to the creation of the user interface, let’s take a minute to think about when the user marks a Square that is not Blank. In this case, we shouldn’t increment the marks count nor change the Square’s state, so let’s add a property in Square called
Now we can update the
mark() method to check if the given Square can change its state:
Creating the User Interface
Start by downloading the pictures I’ve used to represent the Square States here.
Then, extract them them into a new folder app/img.
Creating the General Layout
Let’s draw a quick sketch to display how we want the interface to look like:
In addition to the Board, we would also like to have a button to restart the game, a mini scoreboard and the information about the current player (that we’ll call the “game panel”). This placeholder can also be used to show information of a win, as well as a draw.
Let’s go ahead and create the
Then, declare the
BoardComponent within the
AppComponentModule, located in app/main.ts:
Now, we can display the
BoardComponent by including its selector in app.component.html:
We are now ready to work on the user interface.
Open up app/board.component.html and put the following content:
Notice how we’re able to simply bind the Squares to the GridLayout, since we’re using an Array. We just map each Square’s y position to the corresponding StackLayout’s [col], and x position to the [row].
Then, put the associated styles in app/board.component.css:
And the output should look like this:
Creating the Board Grid
Let’s now work to make the grid actually visible. The first thing we need is the row / column specifications. We could do it statically and simply define 3 rows and 3 columns but that would be best to use the actual board’s size, to make this dynamic. To do this, let’s add a method to the
BoardComponent in order to get the Board “side specification”:
With a size of 3, this will return “*, *, *” (we want every column and every row to occupy as much space as possible).
We will also display the Square placeholders using two different colors in order to create a nice grid effect. Let’s introduce a method within the
BoardComponent in order to get a ‘light-square’ or ‘dark-square’ class according to a given Square position:
We can now define the two CSS styles light-square and dark-square:
We define the StackLayouts to have 100% height and width in order to occupy the whole space within the Grid. Since the Grid has a ‘star’ specification (remember the ‘*’), that means that every Square will be equal in size.
Let’s update the Board GridLayout:
This should give us the following output:
Ok, we now have a visible Board, but the Squares aren’t really squared, so let’s correct this. We will have to do this programatically within the
BoardComponent, so we’ll have to extract the GridLayout from the View. Let’s add the #boardGrid identifier to the GridLayout:
To make the Board grid squared, we’re going to use the device screen dimensions. Currently, when the GridLayout is displayed, the width will occupy the whole screen width, given the ‘star’ specification. That means that we want the height to be set to the screen width as well. If we have a larger width than height though (for instance when using a tablet), we will have to do the opposite. And since the Board is not the only element occupying the device height, we need to take the total height of the other UI elements into consideration. In our case, the overflow of height would be: height of top-panel + margin of top-panel + height of game-panel + margin of game-panel = 50 + 10 + 50 + 10 = 120. Let’s go ahead and implement this:
Output on a regular phone:
On a tablet:
We’re now done for the general layout.
Marking the Squares
Let’s now work on the visual marks of the Squares. The first thing we’re going to do, is to create a Pipe that will help us transform a SquareState into the corresponding Image source path:
Then, add it to the
AppComponentModule declarations list:
Let’s first use this pipe to display the current player state in the game-panel:
And add some styling:
Let’s now use the pipe to display the Squares within the Board grid:
And add some styling:
We now need to be able to call the
mark() method in order to change the state of a Square. Let’s add a
mark() method to the
And bind it to the (tap) event of the Square layout:
Tapping on a Square in the Board grid should now change its state visually:
Animating the Squares After a Win
Now that we’re able to mark the Squares, let’s work on the detection of a win. We already have a method to retrieve potential winning indexes within the Board Squares Array. This Array is bound to a ngForOf hook that spit out the StackLayouts representing our Squares. This means that the order of indexes is conserved, and we can dynamically access each of the StackLayouts using its corresponding index in the Squares Array.
First, we’ll have to be able identify the Square StackLayouts. To do this, we’ll add a #square identifier:
Then, to retrieve each Square StackLayout identified by #square, we’ll add a @ViewChildren property within the Component, and transform it to an Array of StackLayout:
Then, we’re going to edit the
mark() method to get the winning indexes from the Board. If we get some, then we animate the corresponding Squares within the squares ViewChildren property:
That should give us:
Great! let’s now add the information about the win in the game-panel.
Displaying the Game’s Information in the Game Panel
We want to replace the content of the game-panel caption “Next to play” with a one indicating that the current player just won. Let’s introduce a dynamic caption property in the
And bind it to the next-to-play Label in the game-panel:
If you try out this now, you should notice a wrong behaviour though. The displayed winner is actually the one that is supposed to play next, which also get its winning count incremented. This is due to the fact that we always switch the state, even if the game has already been won.
Let’s modify the logic in the Board to prevent the current State from changing if there is a win:
Let’s now handle the case of a draw. If this happens, we only want to display the caption “Draw” within the game panel, without any State Image. We’re going to add a property to return the visibility of the next-to-play Image in the game-panel. If the Board is in a draw state, then we return ‘collapsed’, and ‘visible’ otherwise. We also modify the
gamePanelCaption property to include the draw case:
Now, we can bind the Image visibility to the property we’ve just created:
Restarting the Game
Last step: bind the Restart Button with the start of a new game. Add the following method in the
And bind it to the (tap) event within the Resart Button:
Bonus: Changing the Size of the Board
As you’ve noticed, we took care to make every computation according to the Board size. Try to change it to 6 to see if the algorithms still work:
Looks like it does 😉
That’s it! If you liked this tutorial, please share it. Don’t hesitate to leave a comment for any questions or possible enhancements, I would love to hear about your feedback.