7
\$\begingroup\$

I made this code myself and was experimenting with making buttons from a table, and decided to make a game of Tic-Tac-Toe with it.

The code loads from JSON a grid, and then makes a Tic-Tac-Toe game with populated as a 3x3 table with empty spaces. This takes place in the getJSON. Inside each cell are events, namely the click event, which simulates a turn.

It is complete with a home page and styling. Perhaps the most notable feature is the border-style: inset and border-style: outset, which make tds appear as buttons, which can be utilized with jQuery.

// retrieve game state from json, and append a clickable tic tac toe table
$.getJSON('asset/json/tictactoe.json',
  function(data)
  {
    // add a table to the grid (3x3)
    $("#grid").append('<table/>');
    var table = $("#grid").find("table");
    for (var i=0; i<data.length; ++i)
    {
      // add a row in the table
      $(table).append('<tr/>');
      for (var j=0; j<data[i].length; ++j)
      {
        var row = $(table).children()[i];
        $(row).append('<td/>');
        var td = $(row).children()[j];

        // add the 2D grid from JSON object
        $(td).html(data[i][j]);

        // add id to attribute
        //this is used to assist in checking game state for the end of the game
        $(td).attr("id","b"+i+j);

        // add hover feature to table buttons
        $(td).hover(function() { $(this).toggleClass("hovered"); });

        // make table buttons clickable
        $(td).click(
          function()
          {
            var X = "&nbsp;X&nbsp;"; var O = "&nbsp;O&nbsp;"; var V = "&nbsp;&nbsp;&nbsp;"; var D = "DRAW!";
            if (($("#winner").html() == X) || ($("#winner").html() == O) || ($("#winner").html() == D))
            {
              window.alert('game is complete.');
              return true;
            }
            if ($(this).html() != V)
            {
              window.alert('square is occupied.');
              return false;
            }
            var turn = $("#turn").html();
            $(this).html(turn);
            turn = (turn == O) ? X : O;
            $("#turn").html(turn);
            $(this).toggleClass("pushed");
            $("#winner").trigger("click");
          }
        );
      }
    }
  }
);

// check game state when trigger or if user clicks the winner box
$("#winner").click(
  function() {

    // initialize variables
    var board = [[[],[],[]],[[],[],[]],[[],[],[]]];
    var X = "&nbsp;X&nbsp;"; var O = "&nbsp;O&nbsp;"; var V = "&nbsp;&nbsp;&nbsp;"; var D = "DRAW!";

    // if there is already a winner, or it is drawn, then do not bother
    if (($("#winner").html() == X) || ($("#winner").html() == O) || ($("#winner").html() == D)) return true;

    // retrieve board from td's. could be done by grabbing #grid
    var occupied = 0;
    for (var i=0; i<board.length; ++i)
      for (var j=0; j<board[i].length; ++j)
        board[i][j] = $("#"+"b"+i+j).html();

    var winner = V;

    // check for non-diagonal wins
    for (var i=0; i<board.length; ++i)
    {
      // horizontally
      if (((board[i][0] == X) || (board[i][0] == O)) && ((board[i][0] == board[i][1]) && (board[i][1] == board[i][2])))
      {
        winner = board[i][0];
        break;
      }

      // vertically
      if (((board[0][i] == X) || (board[0][i] == O)) && (board[0][i] == board[1][i]) && (board[1][i] == board[2][i]))
      {
        winner = board[0][i];
        break;
      }
    }

    // if no winner yet
    if (winner == V)
    {

      // check for diagonal wins
      if ((board[0][0] == X) || (board[0][0] == O))
        if ((board[0][0] == board[1][1]) && (board[1][1] == board[2][2]))
          winner = board[0][0];
      if ((board[0][2] == X) || (board[0][2] == O))
        if ((board[0][2] == board[1][1]) && (board[1][1] == board[2][0]))
          winner = board[0][2];

    }

    // is there a winner?
    if (winner != V)
    {
      $("#turn").html("Game Over");
      $("#winner").html(winner);
      return true;
    }

    // count how many suqares are occupied
    var occupied = 0;
    for (var i=0; i<board.length; ++i)
      for (var j=0; j<board[i].length; ++j)
        if ((board[i][j] == X) || (board[i][j] == O)) ++occupied;

    // is the game drawn?
    if (occupied >= 9)
    {
      $("#turn").html("Game Over");
      $("#winner").html(D);
      return true;
    }
  }
);
#grid table {
  border-spacing: 3px;
}

#grid table > tr > td {
  border-color: #92A5BD;
  border-width: 3px;
  border-style: outset;
  padding: 3px;
  font-family: Verdana, Arial, Helvetica, sans-serif;
  font-size: 16px;
  text-align: center;
  cursor: pointer;
}

#grid table > tr > td.hovered {
  border-style: inset; /* button appearance w/ border style */
  background-color: #F99;
}

#grid table > tr > td.pushed {
  border-style: outset; /* button appearance w/ border style */
  background-color: #999;
}
<!doctype html>
<html>
  <head>
    <link rel="stylesheet" href="asset/css/tictactoe.css">
  </head>
  <body>
    <div id="grid"><!-- tic-tac-toe board is populated here -->
    </div>
    <div id="game">
      <table>
      <tr>
        <td>Turn: </td><td id="turn">&nbsp;X&nbsp;</td>
      </tr>
      <tr>
        <td>Winner: </td><td id="winner">&nbsp;&nbsp;&nbsp;</td>
      </tr>
      </table>
    </div>
    <!--
    <div>
      <textarea id="debug" rows="10" cols="50"></textarea>
    </div>
    -->
    <script type="text/javascript" src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
    <script type="text/javascript" src="asset/js/jquery-3.2.1.min.js"></script>
    <script type="text/javascript" src="asset/js/tictactoe.js"></script>
  </body>
</html>

It can perhaps be improved upon with gifs, and externalizing variables somehow, namely the X,O,V,D variables. Also, what is annoying is that the td's don't stay still when the text inside changes, which is why I used the "nbsp" character. A better choice could've been made. All in all, decent result.

\$\endgroup\$
0

2 Answers 2

2
\$\begingroup\$

I found some improvements to make to your Tic Tac Toe game. I think that makes a great example for many things.

Looking at the JS, the first thing I noticed was the .getJSON(), mainly to dynamically create the play grid. I wondered if the intent was to pre-fill the grid, like a saved game, possibly. But I found nothing to save... So I passed on it.

The famous JSON was not provided... So I had to look further anyway to re-create your game.

Next thing I found, and it took quite long to figure out, was the use of the #winner element, which had a click handler on it... But was empty.

Then I found out that its use was because of a programmatically triggered click, just to continue on the next part of what I consider to being part of the same function.

And I really found it strange...

So I took the decision to get rid of the JSON. But not right now. I had to first make it run to actually see the beast. I found a "definition" of it, so I just used it:

var board = [[[],[],[]],[[],[],[]],[[],[],[]]];

Then grid appeared. And I understood how you where using the &nbsp; you mentioned. So I filled the all the level 3 depth arrays with &nbsp;&nbsp;&nbsp;, the V variable.

So that was my start.

I took several decisions then:

  1. I got rid of the JSON (I'm repeating, just to have it listed here)
  2. I got rid of the dynamic table creation... A Tic Tac Toe grid always is 3X3, so why not just place that in the HTML markup?
  3. I apply CSS width and height to the cells. You mentionned the "annoying" effect of the cells not staying still. That fixed it. And all the &nbsp; were then useless.
  4. So I got rid of the X,O,V,D variables too...
  5. I made it only one function, a click handler on the cells.
  6. I also got rid of the hover handler, which CSS does marvelously.
  7. I used a boolean variable to toggle the turns.
  8. I used .data() to set/get the X or O on the cells, instead of .html(). That can enable one to upgrade the game a bit more with some images. I didn't, but it is now possible since the "value" is stored otherwise.

So... Quite a lot, right?

Let's see the code and the comments within:

// Lookup for the cell collection
var cells = $("#grid td");

// Lookup for the game indicators
turn_indicator = $("#turn");
winner_indicator = $("#winner");

// Player turn boolean O = true, X = false
var turn = false; // X starts

// Cell click handler
cells.on("click",function(){
  
  // Who's the active/next player now?
  var active_player = (turn)?"O":"X";
  var next_player = (turn)?"X":"O";
  
  // If cell is occupied.
  if ($(this).data("player") != null){
    window.alert('The cell is occupied.');
    return;
  }
  
  // Check if the game's already over
  if ((winner_indicator.html() != "")){
    window.alert('The game is over!');
    return;
  }
  
  // Mark the cell and change the class to show it's now occupied.
  $(this).data("player",active_player).html(active_player).toggleClass("pushed");
  
  // Switch turn.
  turn = !turn;
  turn_indicator.html(next_player);
  
  //
  // Check for a victory condition from the cells data
  //
  
  // Horizontally.
  for(i=0;i<3;i++){
    var k = 3*i;
    if( cells.eq(k).data("player") == cells.eq(k+1).data("player")
        && cells.eq(k).data("player") == cells.eq(k+2).data("player")
        && cells.eq(k).data("player") != null ){
      
      // Coulour wining cells
      cells.eq(k).addClass("winning_cell");
      cells.eq(k+1).addClass("winning_cell");
      cells.eq(k+2).addClass("winning_cell");
      
      // Announce the winner!
      turn_indicator.html("Game Over!");
      winner_indicator.html(cells.eq(i).data("player"));
      console.log("-")
      return true;
    }
  }
  
  // vertically
  for(i=0;i<3;i++){
    if( cells.eq(i).data("player") == cells.eq(i+3).data("player")
        && cells.eq(i).data("player") == cells.eq(i+6).data("player")
        && cells.eq(i).data("player") != null ){
      
      // Coulour wining cells
      cells.eq(i).addClass("winning_cell");
      cells.eq(i+3).addClass("winning_cell");
      cells.eq(i+6).addClass("winning_cell");
      
      // Announce the winner!
      turn_indicator.html("Game Over!");
      winner_indicator.html(cells.eq(i).data("player"));
      console.log("|");
      return true;
    }
  }
  
  // Diagonal "\"
  if( cells.eq(0).data("player") == cells.eq(4).data("player")
        && cells.eq(0).data("player") == cells.eq(8).data("player")
        && cells.eq(0).data("player") != null ){
    
    // Coulour wining cells
    cells.eq(0).addClass("winning_cell");
    cells.eq(4).addClass("winning_cell");
    cells.eq(8).addClass("winning_cell");
    
    // Announce the winner!
    turn_indicator.html("Game Over!");
    winner_indicator.html(cells.eq(0).data("player"));
    console.log("\\")
    return true;
  }
  
  // Diagonal "/"
  if( cells.eq(2).data("player") == cells.eq(4).data("player")
        && cells.eq(2).data("player") == cells.eq(6).data("player")
        && cells.eq(2).data("player") != null ){
    
    // Coulour wining cells
    cells.eq(2).addClass("winning_cell");
    cells.eq(4).addClass("winning_cell");
    cells.eq(6).addClass("winning_cell");
    
    // Announce the winner!
    turn_indicator.html("Game Over!");
    winner_indicator.html(cells.eq(2).data("player"));
    console.log("/")
    return true;
  }
  
  // Is there still empty cells?
  if($(".pushed").length==9){
    turn_indicator.html("Game Over!");
    winner_indicator.html("Drawn!");
  }
    
  // Game continues...
});
#grid{
  border-spacing: 3px;
}
#grid td{
  border-color: #92A5BD;
  border-width: 3px;
  border-style: outset;
  padding: 3px;
  font-family: Verdana, Arial, Helvetica, sans-serif;
  font-size: 16px;
  text-align: center;
  cursor: pointer;
  width: 3em;
  height: 3em;
}
#grid td:hover{
  border-style: inset; /* button appearance w/ border style */
  background-color: #F99;
}
.pushed{
  border-style: outset; /* button appearance w/ border style */
  background-color: #999;
}
.winning_cell{
  background-color: #1FC01F !important;
}
.indicator{
  display:inline-block;
  padding:0.5em 1em;
  min-width:3em;
}
<!doctype html>
<html>
  <head>
    <link rel="stylesheet" href="asset/css/tictactoe.css">
  </head>
  <body>
    <table id="grid">
      <tr><td></td><td></td><td></td></tr>
      <tr><td></td><td></td><td></td></tr>
      <tr><td></td><td></td><td></td></tr>
    </table>
    <div class="indicator">Turn:</div><div id="turn" class="indicator">X</div><br>
    <div class="indicator">Winner</div><div id="winner" class="indicator"></div>
    <script type="text/javascript" src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
    <script type="text/javascript" src="asset/js/tictactoe.js"></script>
  </body>
</html>

So as you can see... The function reads now from top to bottom and it is easy to understand since it is sequential. The use of return; here, is just to stop the code execution. There is absolutely nothing returned nowhere. So just return; is good. The false is useless in a click handler, except if that is to stop an event. But in this case, it is better to use .preventDefault(). In some other cases, you can return to the caller a boolean, a value, an object, etc....

From the user click, everything executes until that hits a return;. That saves from a big bunch of nested if/else... or calls to a function defined above or below. When you can do it sequentially (it's not always the case), do it!

So that reduces the code a lot. I used 7 if against 13... And 2 for loops against 7.

And I added a "winning cells colouring" feature! ;)

Important to mention, you had the jQuery library loaded twice. It's always odd to do so. It may work... May not.

I reduced the amount of jQuery lookups on the DOM to the minimum. Every time you use $("some_selector"), jQuery works in the background to get you the element's object. If you already have it, you can save on "resources". That is a good habit to develop... It's probably why they call it a "good practice".

And when you have a jQuery object in variable, like var table = $("#grid").find("table");, no need to perform a lookup again on it, like $(table). That's an horrible resource waste. ;)

--
I had to edit... So why not give out a trick of mine, to avoid stupid errors in loops, particularly on nested loops. Don't use j as loop index!! It's terribly too similar to i and so hard to find! I use k... The letter after. ;)

\$\endgroup\$
1
\$\begingroup\$

Louys Patrice Bessette already mentioned in the answer many ways to clean up the code - including eliminating the table creation in JavaScript. However, if that is something that needed to be done, I suggest creating the table and adding all rows and cells before adding the table to the DOM.

So these lines :

$("#grid").append('<table/>');
var table = $("#grid").find("table");

Can be updated to a single call to the jQuery element creation function:

var table = $("<table/>");

That way, we keep a reference the element that was just created and don't have to query the DOM for a reference to it. The same is true for adding rows (and cells). The following lines:

$(table).append('<tr/>');
for (var j=0; j<data[i].length; ++j)
{
    var row = $(table).children()[i];
    $(row).append('<td/>');

Can be replaced with these:

for (var i=0; i<data.length; ++i)
{
    var row = $('<tr/>');
    table.append(row);

And the table cells can be added directly to row:

var td = $('<td/>');
row.append(td);

Then there is no need to wrap td in calls to the jQuery Selector function (i.e. $()).

And after the loop, then add that table element to the DOM:

$("#grid").append(table);

See that portion updated in the snippet below. Note that it doesn't have the AJAX call because there is no local endpoint to supply that. Instead, the function is called with the sample data structure Louys suggested.

If you weren't using jQuery, a DocumentFragment could be used to create the elements and add them all at once to the page. See the section "Use a DOM DocumentFragment in this article for more information about that.

// retrieve game state from json, and append a clickable tic tac toe table
var board = [[[],[],[]],[[],[],[]],[[],[],[]]];

const winnerElement = $("#winner");
const turnElement = $('#turn');
//$.getJSON('asset/json/tictactoe.json',
  (function processData(data)
  {
    // add a table to the grid (3x3)
    //$("#grid").append('<table/>');
    //var table = $("#grid").find("table");
    var table = $("<table/>");
    for (var i=0; i<data.length; ++i)
    {
      // add a row in the table
      //table.append('<tr/>');
      var row = $('<tr/>');
      table.append(row);
      for (var j=0; j<data[i].length; ++j)
      {
        //var row = table.children()[i];
        var td = $('<td/>');
        row.append(td);
        //var td = $(row).children()[j];

        // add the 2D grid from JSON object
        td.html(data[i][j]);

        // add id to attribute
        //this is used to assist in checking game state for the end of the game
        td.attr("id","b"+i+j);

        // add hover feature to table buttons
        td.hover(function() { $(this).toggleClass("hovered"); });

        // make table buttons clickable
        td.click(
          function()
          {
            var X = "&nbsp;X&nbsp;"; var O = "&nbsp;O&nbsp;"; var V = "";/*"&nbsp;&nbsp;&nbsp;";*/ var D = "DRAW!";
            if ((winnerElement.html() == X) || (winnerElement.html() == O) || (winnerElement.html() == D))
            {
              window.alert('game is complete.');
              return true;
            }
            if ($(this).html() != V)
            {
              window.alert('square is occupied.');
              return false;
            }
            var turn = turnElement.html();
            $(this).html(turn);
            turn = (turn == O) ? X : O;
            turnElement.html(turn);
            $(this).toggleClass("pushed");
            winnerElement.trigger("click");
          }
        );
      }
    }
    $("#grid").append(table);
  }
)(board);
// check game state when trigger or if user clicks the winner box
$("#winner").click(
  function() {

    // initialize variables
    var board = [[[],[],[]],[[],[],[]],[[],[],[]]];
    var X = "&nbsp;X&nbsp;"; var O = "&nbsp;O&nbsp;"; var V = "&nbsp;&nbsp;&nbsp;"; var D = "DRAW!";

    // if there is already a winner, or it is drawn, then do not bother
    if ((winnerElement.html() == X) || (winnerElement.html() == O) || (winnerElement.html() == D)) return true;

    // retrieve board from td's. could be done by grabbing #grid
    var occupied = 0;
    for (var i=0; i<board.length; ++i)
      for (var j=0; j<board[i].length; ++j)
        board[i][j] = $("#"+"b"+i+j).html();

    var winner = V;

    // check for non-diagonal wins
    for (var i=0; i<board.length; ++i)
    {
      // horizontally
      if (((board[i][0] == X) || (board[i][0] == O)) && ((board[i][0] == board[i][1]) && (board[i][1] == board[i][2])))
      {
        winner = board[i][0];
        break;
      }

      // vertically
      if (((board[0][i] == X) || (board[0][i] == O)) && (board[0][i] == board[1][i]) && (board[1][i] == board[2][i]))
      {
        winner = board[0][i];
        break;
      }
    }

    // if no winner yet
    if (winner == V)
    {

      // check for diagonal wins
      if ((board[0][0] == X) || (board[0][0] == O))
        if ((board[0][0] == board[1][1]) && (board[1][1] == board[2][2]))
          winner = board[0][0];
      if ((board[0][2] == X) || (board[0][2] == O))
        if ((board[0][2] == board[1][1]) && (board[1][1] == board[2][0]))
          winner = board[0][2];

    }

    // is there a winner?
    if (winner != V)
    {
      turnElement.html("Game Over");
      winnerElement.html(winner);
      return true;
    }

    // count how many suqares are occupied
    var occupied = 0;
    for (var i=0; i<board.length; ++i)
      for (var j=0; j<board[i].length; ++j)
        if ((board[i][j] == X) || (board[i][j] == O)) ++occupied;

    // is the game drawn?
    if (occupied >= 9)
    {
      turnElement.html("Game Over");
      winnerElement.html(D);
      return true;
    }
  }
);
#grid table {
  border-spacing: 3px;
}

#grid table > tr > td {
  border-color: #92A5BD;
  border-width: 3px;
  border-style: outset;
  padding: 3px;
  font-family: Verdana, Arial, Helvetica, sans-serif;
  font-size: 16px;
  text-align: center;
  cursor: pointer;
}

#grid table > tr > td.hovered {
  border-style: inset; /* button appearance w/ border style */
  background-color: #F99;
}

#grid table > tr > td.pushed {
  border-style: outset; /* button appearance w/ border style */
  background-color: #999;
}
<!doctype html>
<html>
  <head>
    <link rel="stylesheet" href="asset/css/tictactoe.css">
  </head>
  <body>
    <div id="grid"><!-- tic-tac-toe board is populated here -->
    </div>
    <div id="game">
      <table>
      <tr>
        <td>Turn: </td><td id="turn">&nbsp;X&nbsp;</td>
      </tr>
      <tr>
        <td>Winner: </td><td id="winner">&nbsp;&nbsp;&nbsp;</td>
      </tr>
      </table>
    </div>
    <!--
    <div>
      <textarea id="debug" rows="10" cols="50"></textarea>
    </div>
    -->
    <script type="text/javascript" src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
    <script type="text/javascript" src="asset/js/jquery-3.2.1.min.js"></script>
    <script type="text/javascript" src="asset/js/tictactoe.js"></script>
  </body>
</html>

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.