DEV Community

Cover image for Creating Tic-Tac-Toe in C# and Blazor WebAssembly
Matthew Jones
Matthew Jones

Posted on

Creating Tic-Tac-Toe in C# and Blazor WebAssembly

Blazor WebAssembly was released a little while ago, and since I'm a fan of all things Blazor, I decided it might be a good time for another game post!

In this post, we're going to build a working C# and Blazor WebAssembly application that can play the children's game tic-tac-toe. Let's get going!

Alt Text
Watch your fingers! Photo by Viktor Talashuk / Unsplash

The Sample Project

As always, there's a sample project on GitHub to go along with this post. Check it out, and pull requests are welcome!

Requirements

To develop in Blazor WebAssembly, we need to download the .NET Core SDK from this page.

You can also use Visual Studio 16.6, Visual Studio Code with the C# extension, or Visual Studio Mac to develop using Blazor WebAssembly. For this project, I used the full Visual Studio.

Creating the Project

The screenshots below are from Visual Studio 2019 v16.6; your process to set up a new Blazor WebAssembly project may be different.

Let's create a new project! In Visual Studio, navigate to File -> New -> Project and select Blazor App:

Alt Text

On the next screen, name your project and solution and specify the directory you want to use, then press Create:

Alt Text

Finally, select "Blazor WebAssembly App":

Alt Text

The resulting project's file directory will look something like this:

Alt Text

If we run the app, we see that we have a fully-functional Blazor application:

Alt Text

Ta-da! We're on our way! Now let's start the hard part: modeling a tic-tac-toe game.

Tic, Tac, Toe, Won!

We're going to take a lot of cues from one of my earlier posts about modeling Connect Four in Blazor.

Before we can write our code, let's first think about the kinds of things (or "models") a game of tic-tac-toe has.

It has a game board, which might look like this:

Alt Text

Such a board has nine spaces on which a player can draw an X or an O.

We also consider the X's and O's themselves. For this demo, we'll call them "Game Pieces" and think of them like tokens you place on the board. So, we'll need a class to model them.

Finally, we need trackers for when the game is won, or if the game is a draw.

Let's get modeling!

Board to Pieces

Let's start with the game pieces, i.e. the X's and O's. The only distinguishing characteristic of these pieces is that each has a letter associated. Let's create an enumeration for those letters, and a class for the pieces.

public enum PieceStyle
{
    X,
    O,
    Blank
}

public class GamePiece
{
    public PieceStyle Style;

    public GamePiece()
    {
        Style = PieceStyle.Blank;
    }
}

Let's also start modeling a class for the game board:

public class GameBoard
{
    //A 2D array of game pieces, 
    //expected to blank at the beginning of the game.
    public GamePiece[,] Board { get; set; }

    public PieceStyle CurrentTurn = PieceStyle.X;

    public GameBoard()
    {
        Reset();
    }
}

Note the constructor method, and the Reset() method called by it. All this method does is reset every space to blank:

public void Reset()
{
    Board = new GamePiece[3, 3];

    //Populate the Board with blank pieces
    for (int i = 0; i <= 2; i++)
    {
        for (int j = 0; j <= 2; j++)
        {
            Board[i, j] = new GamePiece();
        }
    }
}

But now we start the real work: how do we model a game being played?

Children, Take Turns!

Let's consider the kinds of steps we need to take to model a game of tic-tac-toe in progress.

We need to track whose turn it is. In our GameBoard class, we can do this by using a property CurrentTurn, and a method SwitchTurns():

public class GameBoard
{
    //...Other Properties

    public PieceStyle CurrentTurn = PieceStyle.X;

    private void SwitchTurns()
    {
        //This is equivalent to: if currently X's turn, 
        // make it O's turn, and vice-versa
        CurrentTurn = 
            CurrentTurn == PieceStyle.X ? PieceStyle.O : PieceStyle.X;
    }
}

We also need a method to represent a turn being taken. On a player's turn, they will click an empty space and place their marker (X or O) on that space. However, they shouldn't be able to click any spaces in two conditions:

  1. The space is already occupied OR
  2. The game is complete.

Let's deal with the second condition first. We can use a new property to check if the game is complete:

public class GameBoard 
{
    //...Other Properties

    public bool GameComplete => GetWinner() != null || IsADraw();

    //...Other Methods
}

We will implement the methods GetWinner() and IsADraw() later. The main thing to take away from this is that the game is complete when either a winner is declared or a draw occurs.

Now we can write a method to allow players to "claim" spaces, which also includes a check to make sure that they aren't already claimed:

public class GameBoard 
{
    //...Other Properties and Methods

    //Given the coordinates of the space that was clicked...
    public void PieceClicked(int x, int y)
    { 
        //If the game is complete, do nothing
        if (GameComplete) { return; }

        //If the space is not already claimed...
        GamePiece clickedSpace = Board[x, y];
        if (clickedSpace.Style == PieceStyle.Blank)
        {
            //Set the marker to the current turn marker (X or O)
            clickedSpace.Style = CurrentTurn;

            //Make it the other player's turn
            SwitchTurns();
        }
    }
}

That's all the code we need for our tic-tac-toe players to take turns! Now let's deal with some other scenarios.

3, 2, 1... Draw!

NOTE: The code in this section and the next was originally submitted by John Tomlinson and modified by me.

It's possible (and if the players are older than five, likely) that a tic-tac-toe game will end in a draw. A "draw" occurs when all spaces are occupied and no winner can be found. So, let's create a method to watch for this situation.

public class GameBoard 
{
    public bool IsADraw()
    {
        int pieceBlankCount = 0;

        //Count all the blank spaces. 
        //If the count is 0, this is a draw.
        for (int i = 0; i < 3; i++)
        {
            for (int j = 0; j < 3; j++)
            {
                pieceBlankCount = this.Board[i, j].Style == PieceStyle.Blank
                                    ? pieceBlankCount + 1
                                    : pieceBlankCount;
            }
        }

        return pieceBlankCount == 0;
    }
}

The Only Winning Move is Not to Play

Now we come to the most complex of the scenarios we're going to be dealing with: how do we determine if a game has been won?

The method to check this is going to be brute-force: for each space on the board, check each possible direction for matching pieces, and if you find three in a row, stop checking.

First, let's implement an enumeration to show the directions from which we will look for tic-tac-toes:

public enum EvaluationDirection
{
    Up,
    UpRight,
    Right,
    DownRight
}

The method to do this for a single given space looks like this:

public class GameBoard
{
    //...Other Properties and Methods

    private WinningPlay EvaluatePieceForWinner(int i, int j, 
                                               EvaluationDirection dir)
    {
        GamePiece currentPiece = Board[i, j];
        if (currentPiece.Style == PieceStyle.Blank)
        {
            return null;
        }

        int inARow = 1;
        int iNext = i;
        int jNext = j;

        var winningMoves = new List<string>();

        while (inARow < 3)
        {
            //For each direction, increment the pointers 
            //to the next space to be evaluated
            switch (dir)
            {
                case EvaluationDirection.Up:
                    jNext -= 1;
                    break;
                case EvaluationDirection.UpRight:
                    iNext += 1;
                    jNext -= 1;
                    break;
                case EvaluationDirection.Right:
                    iNext += 1;
                    break;
                case EvaluationDirection.DownRight:
                    iNext += 1;
                    jNext += 1;
                    break;
            }

            //If the next "space" is off the board, 
            //don't check it.
            if (iNext < 0 || iNext >= 3 
                || jNext < 0 || jNext >= 3) { break; }

            //If the next space has a matching letter...
            if (Board[iNext, jNext].Style == currentPiece.Style)
            {
                //Add this space to the collection 
                //of winning spaces.
                winningMoves.Add($"{iNext},{jNext}");
                inARow++;
            }
            else //Otherwise, no tic-tac-toe is found 
                 //for this space/direction
            {
                return null;
            }
        }

        //If we found three in a row
        if (inARow >= 3)
        {
            //Return this set of spaces as the winning set
            winningMoves.Add($"{i},{j}");

            return new WinningPlay()
            {
                WinningMoves = winningMoves,
                WinningStyle = currentPiece.Style,
                WinningDirection = dir,
            };
        }

        //If we got here, we didn't find any tic-tac-toes 
        //for the given space.
        return null;
    }
}

But that method only evaluates for a single space; now we need a method to do that for all spaces.

public class GameBoard
{
    public WinningPlay GetWinner()
    {
        WinningPlay winningPlay = null;

        for (int i = 0; i < 3; i++)
        {
            for (int j = 0; j < 3; j++)
            {
                foreach (EvaluationDirection evalDirection in (EvaluationDirection[])Enum.GetValues(typeof(EvaluationDirection)))
                {
                    winningPlay = EvaluatePieceForWinner(i, j, evalDirection);
                    if (winningPlay != null) { return winningPlay; }
                }
            }
        }

        return winningPlay;
    }
}

The above code is the "brute force" I mentioned earlier: a triple-nested loop!

A Couple of Extras

Finally, let's implement two more methods: one which gets a message to display the user when the game is won or drawn, and one to check for those situations so we can display the message.

public class GameBoard
{
    public string GetGameCompleteMessage()
    {
        var winningPlay = GetWinner();
        return winningPlay != null 
               ? $"{winningPlay.WinningStyle} Wins!" : "Draw!";
    }

    public bool IsGamePieceAWinningPiece(int i, int j)
    {
        var winningPlay = GetWinner();
        return winningPlay?.WinningMoves?
                           .Contains($"{i},{j}") ?? false;
    }
}

With that, our GameBoard class is complete! Now we just need to use it on a razor component.

The Component of Your Doom

Here's a basic razor component (which is what Blazor calls the .razor files) that we will modify to host our tic-tac-toe game.

@page "/tictactoe"
@using BlazorWasmTicTacToe.Models;

<h1>Tic-Tac-Toe</h1>

@code {
    GameBoard board = new GameBoard();
}

First, let's write the markup for the board itself.

<div class="board">
    @for (int i = 0; i < 3; i++)
    {
        <div class="column">
            @for (int j = 0; j < 3; j++)
            {
                int x = i;
                int y = j;
                <div class="gamepiece
                         @board.Board[i,j].Style.ToString().ToLower()" 
                         @onclick="@(() => board.PieceClicked(x,y))"
                         style="@(board.IsGamePieceAWinningPiece(i, j)
                                  ? "opacity: 0.6" : "")"></div>
            }
        </div>
    }
</div>

Note that the div with class "gamepiece" has styles dynamically applied to it when it is occupied by a letter and when it is part of the winning tic-tac-toe.

The last little piece is to output a message for wins and draws.

@if (!board.GameComplete)
{
    <h2>@board.CurrentTurn's Turn!</h2>
}
else
{
    <h2>@board.GetGameCompleteMessage() 
        <button class="btn btn-success" 
                @onclick="@(() => board.Reset())">
            Reset
        </button>
    </h2>
}

And with that, we have a working tic-tac-toe game written in C# and Blazor WebAssembly!

GIF Time!

Here's how our tic-tac-toe game looks:

Alt Text

Turned out pretty well, if I do say so myself! But you don't have to believe me, because you can check out the sample project over on GitHub.

Summary

Building a sample Blazor WebAssembly project with C# isn't too hard at all! Just remember that you need very up-to-date IDEs (VS 2019 or VS Code) and to download the SDK from Microsoft's site.

I love the fact that I can write C# and it can be turned into a fully client-side application! So much that I am proud to say:

"Cheers to the coming death of JavaScript!"

Alt Text
Photo by Kelsey Knight / Unsplash

Well, actually, that might be a bit too much. It's probably more like:

"Here's to the mild annoyance of JavaScript developers!"

Yeah, that sounds right.

Thanks for reading, and Happy Coding!

Top comments (0)