Monday 12 August 2019

Web Tutorial: Tic-tac-toe (Part 2/2)

Now, we're going to handle placing "x" or "o" on the board. Modify the HTML. Each square should have the takeTurn() method set to run upon being clicked, with the number of the square and "o" passed in as arguments.
            <div id="board_container">
                <div id="square0" class="square" onclick="user.takeTurn(0, 'o');"></div>
                <div id="square1" class="square" onclick="user.takeTurn(1, 'o');"></div>
                <div id="square2" class="square" onclick="user.takeTurn(2, 'o');"></div>
                <div id="square3" class="square" onclick="user.takeTurn(3, 'o');"></div>
                <div id="square4" class="square" onclick="user.takeTurn(4, 'o');"></div>
                <div id="square5" class="square" onclick="user.takeTurn(5, 'o');"></div>
                <div id="square6" class="square" onclick="user.takeTurn(6, 'o');"></div>
                <div id="square7" class="square" onclick="user.takeTurn(7, 'o');"></div>
                <div id="square8" class="square" onclick="user.takeTurn(8, 'o');"></div>
            </div>


The method itself, of course, will cater to "x" as well. These If blocks are to ensure that it is the appropriate turn and that the game is started. We can't have "x" taking a turn when it's "o"'s turn, for instance. Also, that particular square on the board array must not be occupied, i.e, the value property has to be an empty string.
                takeTurn: function(square, mark)
                {
                    if (mark == game.turn && game.started)
                    {
                        if (game.board[square].value == "")
                        {

                        }
                    }
                },


First, set the value to whatever is appropriate ("x" or "o") determined by the value of mark.
                takeTurn: function(square, mark)
                {
                    if (mark == game.turn && game.started)
                    {
                        if (game.board[square].value == "")
                        {
                            game.board[square].value = mark;
                        }
                    }
                },


Then iterate through the entire winPatterns array and set the appropriate values as well. For instance, if we're setting the square a to "x", then this needs to be set for anything in winPatterns where the square is "a". This makes it easy to check later if victory conditions are met.
                takeTurn: function(square, mark)
                {
                    if (mark == game.turn && game.started)
                    {
                        if (game.board[square].value == "")
                        {
                            game.board[square].value = mark;

                            for (var i = 0; i < game.winPatterns.length; i++)
                            {
                                for (var j = 0; j < game.winPatterns[i].length; j++)
                                {
                                    if (game.winPatterns[i][j].square == game.board[square].square)
                                    {
                                        game.winPatterns[i][j].value = mark;
                                    }  
                                }
                            }
                        }
                    }
                },


And then we modify the class of the appropriate square, adding the value of mark as a CSS class.
                takeTurn: function(square, mark)
                {
                    if (mark == game.turn && game.started)
                    {
                        if (game.board[square].value == "")
                        {
                            game.board[square].value = mark;

                            for (var i = 0; i < game.winPatterns.length; i++)
                            {
                                for (var j = 0; j < game.winPatterns[i].length; j++)
                                {
                                    if (game.winPatterns[i][j].square == game.board[square].square)
                                    {
                                        game.winPatterns[i][j].value = mark;
                                    }  
                                }
                            }

                            document.getElementById("square" + square).className = "square " + mark;
                        }
                    }
                },


Let's modify the CSS some more. While the cursor property for square is pointer, we want to hammer home the point that any occupied square is not clickable (notice that in the takeTurn() method, clicking does nothing if the square is not empty), so we set the cursor property to default.
            .square
            {
                width: 100px;
                height: 100px;
                margin-right: 10px;
                margin-bottom: 10px;
                float: left;
                outline: 1px solid #444444;
                background-color: #FFFFFF;
                cursor: pointer;
            }

            .x, .o
            {
                cursor: default;
            }

            #user_x_container
            {
                color: #FF4400;
            }


Use the before pseudoselector for x and o. We basically want it to fill up the inside of its parent, with maybe some padding. Font properties should be set to personal taste.
            .x, .o
            {
                cursor: default;
            }

            .x:before, .o:before
            {
                display: block;
                width: 80%;
                height: 80%;
                margin: 10% auto 0 auto;
                font-weight: bold;
                font-size: 3em;
                text-align: center;
            }

            #user_x_container
            {
                color: #FF4400;
            }


For x, the content is "X" and the color is orange. For o, content is "O" and color is green.
            .x, .o
            {
                cursor: default;
            }

            .x:before, .o:before
            {
                display: block;
                width: 80%;
                height: 80%;
                margin: 10% auto 0 auto;
                font-weight: bold;
                font-size: 3em;
                text-align: center;
            }

            .x:before
            {
                content: "X";
                color: #FF4400;
            }

            .o:before
            {
                content: "O";
                color: #44FF00;
            }

            #user_x_container
            {
                color: #FF4400;
            }


Go on, click on an empty square. An "O" should appear! Once it appears, mouse over that square and you should see your mouse cursor go back to default.


Back to the myTurn() method. We've established what happens when player "o" (the user) takes his or her turn. But what happens when the player "x" (the computer) takes its turn? We first set up a delay of one second.
                myTurn: function()
                {
                    document.getElementById("user_x_text").innerHTML = "";
                    document.getElementById("user_o_text").innerHTML = "";
                    document.getElementById("user_" + game.turn + "_text").innerHTML = "My turn!";

                    if (game.turn == "x")
                    {
                        setTimeout
                        (
                            function()
                            {

                            },
                            1000
                        )  
                    }
                }


Then we declare randomBlankSquare and set it to the returned value of the getRandomBlankSquare() method of the game object. Let's not bother about that method for now; but the plan is, it should return an integer which is the index of any one of the elements in the board array that has not yet been filled. And if all squares are filled, it returns null.
                        setTimeout
                        (
                            function()
                            {
                                var randomBlankSquare = game.getRandomBlankSquare();

                                if (randomBlankSquare == null)
                                {

                                }
                                else
                                {

                                }
                            },
                            1000
                        )  


If all squares are filled, display a message in the appropriate user's text box signifying that it's a draw. And set the started property of the game object to false to signify that the game is over.
                        setTimeout
                        (
                            function()
                            {
                                var randomBlankSquare = game.getRandomBlankSquare();

                                if (randomBlankSquare == null)
                                {
                                    document.getElementById("user_" + game.turn + "_text").innerHTML = "Looks like it's a draw.";
                                    game.started = false;
                                }
                                else
                                {

                                }
                            },
                            1000
                        )


And if there's a blank square, run the takeTurn() method of the user object, passing in the value of randomBlankSquare and "x". Which basically means player "x" places the mark on that square.
                        setTimeout
                        (
                            function()
                            {
                                var randomBlankSquare = game.getRandomBlankSquare();

                                if (randomBlankSquare == null)
                                {
                                    document.getElementById("user_" + game.turn + "_text").innerHTML = "Looks like it's a draw.";
                                    game.started = false;
                                }
                                else
                                {
                                    user.takeTurn(randomBlankSquare, "x");
                                }
                            },
                            1000
                        )


Wait... what happens if all squares are filled when it's player "o"'s turn?

Ah, I see the gears are turning! Logically, that never happens. The draw condition only happens when all 9 squares are filled. And that means 9 turns have to be taken. Player "o" always starts first... so if all 9 squares are filled, player "o" is always the one filling in the last square.

Back to the game!

We're going to fill in the getRandomBlankSquare() method. First, declare blanks. Set blanks to the array returned when you run the filter() method on the board array, returning only objects whose value properties are empty.
                getRandomBlankSquare: function()
                {
                    var blanks = this.board.filter(function (x) {return x.value == ""});
                },


If blanks is an empty array, return null.
                getRandomBlankSquare: function()
                {
                    var blanks = this.board.filter(function (x) {return x.value == ""});

                    if (blanks.length == 0)
                    {
                        return null;
                    }
                    else
                    {

                    }
                },


Otherwise, run the getRandomNumber() method, entering 0 and the length of blanks minus 1 as the minimum and maximum values.
                getRandomBlankSquare: function()
                {
                    var blanks = this.board.filter(function (x) {return x.value == ""});

                    if (blanks.length == 0)
                    {
                        return null;
                    }
                    else
                    {
                        var rand = this.getRandomNumber(0, blanks.length - 1);
                    }
                },


Next up is the getRandomNumber() method, and that's self-explanatory. We've been using that ad nauseam in web tutorials.
                getRandomNumber: function(min, max)
                {
                    return Math.floor((Math.random() * (min - max + 1)) + max);
                },


So now that you have a random number, run the findIndex() method on the board array. Compare the square property of the random element of blanks against the square property of each element in board, and return the matching index! Thus, if the square property is "a", then you will return 0. If it is "f", you return 5.
                getRandomBlankSquare: function()
                {
                    var blanks = this.board.filter(function (x) {return x.value == ""});

                    if (blanks.length == 0)
                    {
                        return null;
                    }
                    else
                    {
                        var rand = this.getRandomNumber(0, blanks.length - 1);
                        return this.board.findIndex(function (x) {return x.square == blanks[rand].square});  
                    }
                },


OK, now take a turn on the board! The computer should respond with "My Turn!" and place an "X" on the board, then make it your turn again!


For the next step, remember that once a turn is taken, we want to check for a victory condition. That's where the checkWin() method of the game object comes in. Pass in mark as an argument.
                takeTurn: function(square, mark)
                {
                    if (mark == game.turn && game.started)
                    {
                        if (game.board[square].value == "")
                        {
                            game.board[square].value = mark;

                            for (var i = 0; i < game.winPatterns.length; i++)
                            {
                                for (var j = 0; j < game.winPatterns[i].length; j++)
                                {
                                    if (game.winPatterns[i][j].square == game.board[square].square)
                                    {
                                        game.winPatterns[i][j].value = mark;
                                    }  
                                }
                            }

                            document.getElementById("square" + square).className = "square " + mark;

                            if (game.checkWin(mark))
                            {

                            }
                            else
                            {

                            }  
                        }
                    }
                },


If it's not a victory condition, run the nextTurn() method of the game object, signifying that the game goes on.
                            if (game.checkWin(mark))
                            {

                            }
                            else
                            {
                                game.nextTurn();
                            }  


If the game is won, put a victory message in the appropriate user's div. And then set the started property of the game object to false, because the game is over.
                            if (game.checkWin(mark))
                            {
                                document.getElementById("user_" + mark + "_text").innerHTML = "I win!";
                                game.started = false;
                            }
                            else
                            {
                                game.nextTurn();
                            }


The next thing to do is fill out the checkWin() method. By default, it returns false.
                checkWin: function(mark)
                {
                    return false;
                }  


Declare the findSquare variable and iterate through the winPatterns array using a For loop.
                checkWin: function(mark)
                {
                    var findSquare;

                    for (var i = 0; i < this.winPatterns.length; i++)
                    {
  
                    }

                    return false;
                }  


For each array inside the winPatterns array, check each object. If all three of the value properties of the array objects are set to mark, that means victory has been achieved. So return true.
                checkWin: function(mark)
                {
                    var findSquare;

                    for (var i = 0; i < this.winPatterns.length; i++)
                    {
                        if (this.winPatterns[i][0].value == mark && this.winPatterns[i][1].value == mark && this.winPatterns[i][2].value == mark)
                        {
                            return true;
                        }  
                    }

                    return false;
                }  


But of course, we need a visual indicator. So iterate through that array. Use the findIndex() method to get the index of the squares that have been identified as meeting the victory condition, and set the index to the value of findSquare. Then use findSquare to set add the CSS class win to that element.
                checkWin: function(mark)
                {
                    var findSquare;

                    for (var i = 0; i < this.winPatterns.length; i++)
                    {
                        if (this.winPatterns[i][0].value == mark && this.winPatterns[i][1].value == mark && this.winPatterns[i][2].value == mark)
                        {
                            for (var j = 0; j < 3; j++)
                            {
                                var winPatterns = this.winPatterns;
                                findSquare = this.board.findIndex(function (x) {return x.square == winPatterns[i][j].square});
                                document.getElementById("square" + findSquare).className += " win";                      
                            }

                            return true;
                        }  
                    }

                    return false;
                }  


Here's the styling for win. We basically set the background color to yellow.
            .o:before
            {
                content: "O";
                color: #44FF00;
            }

            .win
            {
                background-color: #FFFF00;
            }

            #user_x_container
            {
                color: #FF4400;
            }


Try it now. When you achieve victory, it should look like this. And clicking will not achieve anything more.


If it's a draw, it should look like this.


Well, that was fun...

... and brought back memories of having a pencil, paper and being a stupid kid.

xoxo,
T___T

No comments:

Post a Comment