cssAudio - Activefile-genericCSS - ActiveGeneric - ActiveHTML - ActiveImage - ActiveJS - ActiveSVG - ActiveText - Activefile-genericVideo - ActiveLovehtmlicon-new-collectionicon-personicon-teamlog-outoctocatpop-outspinnerstartv

Pen Settings

CSS Base

Vendor Prefixing

Add External CSS

These stylesheets will be added in this order and before the code you write in the CSS editor. You can also add another Pen here, and it will pull the CSS from it. Try typing "font" or "ribbon" below.

Quick-add: + add another resource

Add External JavaScript

These scripts will run in this order and before the code in the JavaScript editor. You can also link to another Pen here, and it will run the JavaScript from it. Also try typing the name of any popular library.

Quick-add: + add another resource

Code Indentation

     

Save Automatically?

If active, Pens will autosave every 30 seconds after being saved once.

Auto-Updating Preview

If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.

            
              <!-- hyperlinks: target="_blank" for codepen.io -->

  <div class="container jumbotron jumbotron-mod min-h-100 p-0">
    <div class="row stretch-vert justify-content-center align-items-between">
      <div class="col-12 text-center">
        <h1 class="mt-2">Tic Tac Toe</h1>
        <button id="reset" type="button" class="btn btn-mod-small btn-outline-danger">Reset</button>
      </div>

      <!-- grid -->
      <div id="grid" class="section col-12 text-center my-auto">
        <div class="row justify-content-center">
          <div class="col-12">
            <h2 id="caption" class="text-danger pt-0 mt-0 pb-3">A game for the ages...</h2>
          </div>
          <div class="col-12">
            <h2 id="thinking" class="text-danger pt-0 mt-0 pb-3" style="opacity:0;">Hmm...</h2>
          </div>
        </div>
        <div class="row justify-content-center">
          <div id="sq0" value="0" class="p-0 gridblock border-on-right">
            <span class="tally">x</span>
          </div>
          <div id="sq1" value="1" class="p-0 gridblock border-on-right">
            <span class="tally">o</span>
          </div>
          <div id="sq2" value="2" class="p-0 gridblock">
            <span class="tally">x</span>
          </div>
        </div>
        <div class="row justify-content-center">
          <div id="sq3" value="3" class="p-0 gridblock border-on-right border-on-top">
            <span class="tally">o</span>
          </div>
          <div id="sq4" value="4" class="p-0 gridblock border-on-right border-on-top">
            <span class="tally">x</span>
          </div>
          <div id="sq5" value="5" class="p-0 gridblock border-on-top">
            <span class="tally">x</span>
          </div>
        </div>
        <div class="row justify-content-center">
          <div id="sq6" value="6" class="p-0 gridblock border-on-right border-on-top">
            <span class="tally">o</span>
          </div>
          <div id="sq7" value="7" class="p-0 gridblock border-on-right border-on-top">
            <span class="tally">x</span>
          </div>
          <div id="sq8" value="8" class="p-0 gridblock border-on-top">
            <span class="tally">o</span>
          </div>
        </div>
      </div>

      <!-- Number of Players Selection -->
      <div id="num-players" class="section col-12 text-center my-auto" style="display:none;">
        <h1>How many players?</h1>
        <div class="row p-2 justify-content-center">
          <div class="col-3 col-lg-2">
            <button type="button" class="btn btn-outline-danger w-100 btn-mod num-players-btn" value="1">1</button>
          </div>
          <div class="col-1">
          </div>
          <div class="col-3 col-lg-2">
            <button type="button" class="btn btn-outline-danger w-100 btn-mod num-players-btn" value="2">2</button>
          </div>
        </div>
      </div>

      <!-- Player 1 Symbol Selection -->
      <div id="choose-p1-sym" class="section col-12 text-center my-auto" style="display:none;">
        <h1>Player 1, </h1>
        <h1>choose your symbol:</h1>
        <div class="row p-2 justify-content-center">
          <div class="col-3 col-lg-2">
            <button type="button" class="btn btn-outline-danger w-100 btn-mod choose-p1-sym-btn" value="x">x</button>
          </div>
          <div class="col-1">
          </div>
          <div class="col-3 col-lg-2">
            <button type="button" class="btn btn-outline-danger w-100 btn-mod choose-p1-sym-btn" value="o">o</button>
          </div>
        </div>
      </div>

      <!-- For Messages -->
      <div id="message" class="section col-12 text-center my-auto" style="display:none;">
        <h1></h1>
      </div>

      <div class="col-12 text-center muted mt-auto">
        <p class="pt-5">Designed and Coded by <a class="text-danger" href="http://www.gregwong.me" target="_blank">Greg Wong</a></p>
      </div>
    </div>
  </div>
            
          
!
            
              html, body {
  /* use min-height for when the content is longer the screen */
  min-height: 100vh!important;
  font-family: 'Permanent Marker', cursive;
}

.min-h-100 {
  /* use min-height for when the content is longer the screen */
  min-height: 100vh!important;
}
.stretch-vert {
  min-height: 100vh!important;
}

.jumbotron-mod {
  background-color: #F3E5D1;
  margin-bottom: 0px!important;
}

.btn-mod-small {
  font-family: 'Permanent Marker', cursive;
}

.btn-mod {
  font-family: 'Permanent Marker', cursive;
  font-size: 3em;
}

.gridblock {
  width: 100px;
  height: 100px;
}

.border-on-top {
  border-top: 5px solid black;
}

.border-on-right {
  border-right: 5px solid black;
}

.tally {
  font-size: 6em;
  line-height: 75%;
}

.color-win {
  color: red!important;

  -webkit-transition: all 0.7s ease;
  -moz-transition: all 0.7s ease;
  -o-transition: all 0.7s ease;
  transition: all 0.7s ease;
}

            
          
!
            
              /* --- board.js --- */
var Board = function() {
  const ROWS = 3;
  const COLS = 3;
  var grid = Array(ROWS*COLS).fill(null);

  const SPACES = ROWS * COLS;
  var turns;

  var wins;

  var p1, p2, currPlayer;

  this.init = function(p1sym) {
    init(p1sym);
  };

  this.getCurrentPlayer = function() {
    return currPlayer;
  };

  this.makeMove = function(index) {
    // currPlayer is either 'x' or 'o'
    // index is from 0 - 8
    // return false if problems encountered, else true
    if (p1 == undefined) {
      console.error("init() not called yet");
      return false;
    }
    if (isFull() || hasWinner()) {
      console.error("game over, call init() again");
      return false;
    }
    if (index < 0 || index > 8) {
      console.error("index should be a number between 0 and 8 (inclusive)");
      return false;
    }
    if (grid[index] != null) { // if grid is already occupied
      return false;
    }
    grid[index] = currPlayer;
    updateWinner();
    turns++;
    // switch to other player's turn
    currPlayer = currPlayer == p1 ? p2 : p1;
    return true;
  };

  this.getGrid = function() {
    return grid.slice(); // return a copy so that it can't be modified
  };

  this.isFull = function() {
    return isFull();
  };

  this.getWinner = function() {
    return wins;
  };

  this.getP1Symbol = function() {
    return p1;
  }

  function init(p1sym) {
    turns = 0;
    wins = [];
    grid.fill(null);
    if (p1sym == 'o') {
      p1 = 'o';
      p2 = 'x';
    }
    else {
      p1 = 'x';
      p2 = 'o';
    }
    currPlayer = p1;
  }

  function updateWinner () {
    // if no winner, wins is []
    // if has winner, wins is [ [winner, index1, index2, index3], ... ]

    var winner = getFullRows();
    if (winner) {
      wins.push(winner);
    }
    winner = getFullCols();
    if (winner) {
      wins.push(winner);
    }
    winner = getFullDiagonals();
    if (winner) {
      wins.push(winner);
    }
  }

  function hasWinner() {
    return wins.length > 0;
  }

  function getFullRows() {
    // only for 3 elements
    for (var i = 0; i < ROWS; i++) {
      var i1 = i * COLS;
      var i2 = i * COLS + 1;
      var i3 = i * COLS + 2;
      if (grid[i1] != null && grid[i1] == grid[i2] && grid[i2] == grid[i3]) {
        return [grid[i1], i1, i2, i3];
      }
    }
    return null;
  }

  function getFullCols() {
    // only for 3 elements
    for (var i = 0; i < COLS; i++) {
      var i1 = i;
      var i2 = i + COLS;
      var i3 = i + 2*COLS;
      if (grid[i1] != null && grid[i1] == grid[i2] && grid[i2] == grid[i3]) {
        return [grid[i1], i1, i2, i3];
      }
    }
    return null;
  }

  function getFullDiagonals(){
    // only for 3 elements
    var i1 = 0;
    var i2 = COLS + 1;
    var i3 = 2*COLS + 2;
    if (grid[i1] != null && grid[i1] == grid[i2] && grid[i2] == grid[i3]) {
      return [grid[i1], i1, i2, i3];
    }
    var i1 = COLS - 1;
    var i2 = 2*COLS - 2;
    var i3 = 3*COLS - 3;
    if (grid[i1] != null && grid[i1] == grid[i2] && grid[i2] == grid[i3]) {
      return [grid[i1], i1, i2, i3];
    }
    return null;
  }

  function isFull() {
    return turns >= SPACES;
  }
};

/* --- ai.js --- */
var AI = function() {
  // assume computer is always p2
  var grid;
  var firstMove, secondMove, thirdMove;
  var center_oppositeCorner;
  var corner_oppositeEdges, corner_oppositeCorner;
  var edge_oppositeEdge, edge_adjacentEdges, edge_oppositeCorners;
  const WINNING_COMBOS = [
    [0,1,2],
    [3,4,5],
    [6,7,8],
    [0,3,6],
    [1,4,7],
    [2,5,8],
    [0,4,8],
    [2,4,6],
  ];
  const CORNERS = [0,2,6,8];
  const EDGES = [1,3,5,7];
  const OPPOSITES = [8,7,6,5,4,3,2,1,0];
  const CORNER_OPPOSITE_EDGES = {
    0: [5,7],
    2: [3,7],
    6: [1,5],
    8: [1,3],
  };
  const EDGE_ADJACENT_EDGES = {
    1: [3,5],
    3: [1,7],
    5: [1,7],
    7: [3,5],
  };
  const EDGE_OPPOSITE_CORNERS = {
    1: [6,8],
    3: [2,8],
    5: [0,6],
    7: [0,2],
  };
  const EDGE_ADJACENT_CORNERS = {
    1: [0,2],
    3: [0,6],
    5: [2,8],
    7: [6,8],
  };
  const EDGE_ADJACENT_OPPOSITE_EDGE = {
    1: {
      6:5,
      8:3,
    },
    3: {
      2:7,
      8:1,
    },
    5: {
      0:7,
      6:1,
    },
    7: {
      0:5,
      2:3,
    },
  };

  this.init = function() {
    grid = Array(9).fill(null);
    firstMove = null;
    secondMove = null;
    thirdMove = null;
    center_oppositeCorner = null;
    corner_oppositeEdge = null;
    corner_oppositeCorner = null;
    edge_oppositeEdge = null;
    edge_adjacentEdge = null;
    edge_oppositeCorner = null;
  };

  this.getNextMove = function(p1move) {
    grid[p1move] = 1;
    var nextMove;
    if (firstMove == null) { // if it's not defined
      firstMove = p1move;
      secondMove = getSecondMove();
      nextMove = secondMove;
    }
    else if (thirdMove == null) { // if it's not defined
      thirdMove = p1move;
      nextMove = getFourthMove();
    }
    else { // 6th or 8th move of the game
      nextMove = conquerAndBlockOrRandom();
    }
    grid[nextMove] = 2;
    return nextMove;
  };

  function getSecondMove() {
    if (firstMove == 4) { // center
      // place in corner
      var secondMove = chooseOneAvailable(CORNERS);
      oppositeCorner = OPPOSITES[secondMove];
      return secondMove;
    }
    else if (CORNERS.includes(firstMove)) {
      corner_oppositeEdges = CORNER_OPPOSITE_EDGES[firstMove]
      corner_oppositeCorner = OPPOSITES[firstMove];
    }
    else { // EDGES.includes(firstMove)
      edge_adjacentEdges = EDGE_ADJACENT_EDGES;
      edge_oppositeCorners = EDGE_OPPOSITE_CORNERS;
      edge_oppositeEdge = OPPOSITES[firstMove];
    }
    return 4; // return 4 for both corner and edge cases
  }

  function getFourthMove() {
    var fourthMove;

    if (firstMove == 4 && thirdMove == oppositeCorner) {
      fourthMove = chooseOneAvailable(CORNERS);
    }
    else if (CORNERS.includes(firstMove)) {
      if (CORNER_OPPOSITE_EDGES[firstMove].includes(thirdMove)) {
        fourthMove = corner_oppositeCorner;
      }
      else if (thirdMove == corner_oppositeCorner) {
        fourthMove = chooseOneAvailable(EDGES);
      }
    }
    else if (EDGES.includes(firstMove)) {
      if (thirdMove == edge_oppositeEdge) {
        fourthMove = chooseOneAvailable(CORNERS);
      }
      else if (EDGE_OPPOSITE_CORNERS[firstMove].includes(thirdMove)) {
        fourthMove = EDGE_ADJACENT_OPPOSITE_EDGE[firstMove][thirdMove];
      }
      else if (EDGE_ADJACENT_EDGES[firstMove].includes(thirdMove)) {
        fourthMove = chooseOneAvailable(EDGE_ADJACENT_CORNERS[firstMove]);
      }
    }

    if (fourthMove == null) {
      fourthMove = conquerAndBlockOrRandom();
    }
    return fourthMove;
  }

  function getWinningMove(player) {
    // player should be either 1 or 2
    for (let combo of WINNING_COMBOS) {
      if (grid[combo[0]] == player && grid[combo[1]] == player && grid[combo[2]] == null) {
        return combo[2];
      }
      else if (grid[combo[0]] == player && grid[combo[1]] == null && grid[combo[2]] == player) {
        return combo[1];
      }
      else if (grid[combo[0]] == null && grid[combo[1]] == player && grid[combo[2]] == player) {
        return combo[0];
      }
    }
    return null;
  }

  function chooseOneAvailable(indices) {
    var emptyIndices = indices.filter(function(val){
      return grid[val] == null;
    });
    return emptyIndices[Math.floor(Math.random() * emptyIndices.length)];
  }

  function conquerAndBlockOrRandom() {
    var nextMove;
    // conquer
    nextMove = getWinningMove(2);
    if (nextMove != null) {
      return nextMove;
    }
    // block
    nextMove = getWinningMove(1);
    if (nextMove != null) {
      return nextMove;
    }
    // nextMove == null
    return getRandomMove();
  }

  function getRandomMove() {
    var emptyIndices =
    grid.map(function(val, index){
      if (val == null) return index;
    }).filter(function(val){
      return val != undefined;
    });

    return chooseOneAvailable(emptyIndices);
  }

};

/* --- game.js --- */
var Game = function(animateMakeMove, animateWin, displayMessage, pauseAnimation) {
  var board = new Board();
  var animateMakeMove = animateMakeMove;
  var animateWin = animateWin;
  var displayMessage = displayMessage;
  var pauseAnimation = pauseAnimation;

  var waiting;
  var aiModeOn;
  var ai = new AI();

  this.init = function(isSinglePlayer, p1sym) {
    board.init(p1sym);
    aiModeOn = isSinglePlayer;
    if (aiModeOn) {
      ai.init();
    }
    waiting = true;
  };

  this.makeMove = function(index) {
    index = parseInt(index);
    if (waiting) { // don't do anything if it's already running
      if (board.getCurrentPlayer() == undefined) {
        console.error("init() not called yet");
        return false;
      }
      waiting = false;
      if (!makeMove(index)){
        // returns false for invalid move
        // don't continue if invalid
        waiting = true;
        return false;
      }

      if (aiModeOn) {
        var wins = board.getWinner();
        if (wins.length <= 0 && !board.isFull()){
          pauseAnimation(function() {
            var nextMove = ai.getNextMove(index);
            makeMove(nextMove, true);
            waiting = true;
          });
        }
      }
      else {
        waiting = true;
      }
    }
  };

  this.getGrid = function() {
    return board.getGrid();
  };

  this.currPlayerSymbol = function() {
    return currPlayer;
  };

  function makeMove(index, byAI=false) {
    var currPlayer = board.getCurrentPlayer();
    var valid = board.makeMove(index);
    if (!valid){
      return false;
    }
    else {
      animateMakeMove(currPlayer, index);
    }

    var wins = board.getWinner();
    if (wins.length > 0) { // there is a winner
      for (let winCombo of wins) {
        // display winning animation
        animateWin(winCombo.slice(1)); // flash the three winning squares
      }
      // display winning message with name as argument
      var name;
      if (byAI) {
        name = "Computer";
      }
      else {
        name = wins[0][0] == board.getP1Symbol() ? "Player 1" : "Player 2";
      }
      var message = name + " won!";
      displayMessage(message);
    }
    else if (board.isFull()) {
      displayMessage("It's a tie!");
    }
    return true;
  }
};

/* --- interface.js --- */
var game;
var isSinglePlayer;
var p1sym;

var askNumPlayers = function() {
  $("#num-players").fadeIn();
};

var chooseP1Sym = function() {
  $("#choose-p1-sym").fadeIn();
};

var animateMakeMove = function(currPlayer, index) {
  // this refers to the gridblock
  $("#sq"+index).find('span').text(currPlayer);
};

var pauseAnimation = function(callback) {
  $("#thinking").fadeTo(200,1).delay(400).fadeTo(200, 0, callback);
};

var animateWin = function(arrCombo) {
  $("#reset").css('visibility', 'hidden');
  for (let index of arrCombo) {
    $("#sq"+index).find('span').addClass('color-win');
  }
};

var displayMessage = function(message) {
  $("#reset").css('visibility', 'hidden');
  $("#message").find("h1").text(message);
  $("#grid").delay(1300).fadeOut(400, function() {
    $("#message").fadeIn().delay(1000).fadeOut(400, letsStartOver);
  });
};

var letsStartOver = function() {
  $("#message").find("h1").text("Let's start over again.");
  $("#message").fadeIn(400, function() {
    $(this).delay(1000).fadeOut(400, startGame);
  });
};

var startGame = function() {
  game.init(isSinglePlayer, p1sym);
  $('.gridblock').find('span').removeClass('color-win').text("");
  $("#caption").hide();
  $("#reset").css('visibility', 'visible');
  $("#grid").fadeIn();
};

$(document).ready(function() {

  $("#reset").css('visibility', 'hidden');

  game = new Game(animateMakeMove, animateWin, displayMessage, pauseAnimation);

  // set up the 2 buttons to return either one or two players
  $('.num-players-btn').click(function() {
    var numPlayers = $(this).attr('value');
    isSinglePlayer = numPlayers == '1' ? true : false;
    $("#num-players").fadeOut(400, chooseP1Sym);
  });

  // set up the 2 buttons to return value for p1sym
  $('.choose-p1-sym-btn').click(function() {
    var sym = $(this).attr('value');
    p1sym = sym == 'x' ? 'x' : 'o';
    $("#choose-p1-sym").fadeOut(400, startGame);
  });

  $("#reset").click(function() {
    $(this).css('visibility', 'hidden');
    $(".section:visible").fadeOut(askNumPlayers);
  });

  $("#grid").delay(1000).fadeOut(1000, askNumPlayers);
  // callback chain:
  // askNumPlayers() -> chooseP1Sym() -> startGame()

  // set up the 9 squares to listen to clicks
  $('.gridblock').click(function() {
    var index = $(this).attr('value');
    game.makeMove(parseInt(index));
  });

});

            
          
!
999px
Loading ..................

Console