JSFiddle - React, Tailwind, and code Playground

by Travis Almand

HTML

<button id="startBtn">Start</button>

SCSS

@import url('https://fonts.googleapis.com/css?family=Permanent+Marker');

html {
  box-sizing: border-box;
  font-size: 10px;
}
*, *:before, *:after {
  box-sizing: inherit;
}

body {
  align-items: center;
  display: flex;
  flex-direction: column;
  font-family: 'Permanent Marker';
  height: 100vh;
  justify-content: center;
}
#startBtn {
  background: white;
  border: 2px solid black;
  border-radius: 5px;
  cursor: pointer;
  font-family: 'Permanent Marker';
  font-size: 2rem;
  margin: 0 1rem;
  padding: 10px 40px;
  transition: transform 0.25s;
  
  &:active {
    transform: scale3d(.75, 0.75, 1);
  }
}

#scoreboard {
  display: flex;
  margin-bottom: 4rem;
  width: 50vh;
  
  // display isn't necessary for pseudo elements if parent is flex
  // nice
  &::before {
    // here's a reason for all the dataset changes in js
    // free UI updates with state changes
    content: 'x: ' attr(data-x-score);
    font-size: 3rem;
    width: 50%;
  }
  &::after {
    content: 'o: ' attr(data-o-score);
    font-size: 3rem;
    text-align: right;
    width: 50%;
  }
}

#gameboard {
  border: 1px solid black;
  display: flex;
  flex-wrap: wrap;
  height: 50vh;
  min-height: 200px;
  min-width: 200px;
  width: 50vh;
  
  &::before {
    font-size: 3rem;
    position: absolute;
    text-align: center;
    transform: translate3d(0, -110%, 0);
    width: 50vh;
  }
  // more dataset changes in js causing UI changes
  &[data-message="x"]::before {
    content: 'x turn';
  }
  &[data-message="o"]::before {
    content: 'o turn';
  }
  &[data-message="winner"]::before {
    color: green;
    content: 'winner!';
  }
  &[data-message="tie"]::before {
    color: red;
    content: 'tie!';
  }
  
  // sometimes sub-pixel math causes gaps between borders
  // a more clever border layout would solve that
  .square {
    align-items: center;
    background-color: white;
    border: 1px solid black;
    cursor: pointer;
    display: flex;
    height: 33.33%;
    justify-content: center;
...

JavaScript

console.clear();

var startGame = (function () {
	var _startButton = document.querySelector('#startBtn');
  
  var _startClickHandler = function () {
  	_startButton.remove();
    
    window.setTimeout(function () {
	    scoreboard.createScoreboard();
    }, 250);
    
    window.setTimeout(function () {
	    gameboard.createGameboard();
      gameboard.setMessage('x');
    }, 500);
    
    window.setTimeout(function () {
	    buttonPanel.createPanel();
    }, 750);
  };
  
  _startButton.addEventListener('click', _startClickHandler);
})();

var scoreboard = (function () {
  var public = {};
  
  var _scoreboard;
  var _score = {
  	x: 0,
    o: 0
  };
  
  public.createScoreboard = function () {
  	document.body.insertAdjacentHTML('afterbegin', '<div id="scoreboard"></div>');
    
    _scoreboard = document.querySelector('#scoreboard')
    _scoreboard.dataset.xScore = _score.x
    _scoreboard.dataset.oScore = _score.o;
  };
  
  public.updateScore = function (winner) {
  	_score[winner]++;
  	_scoreboard.dataset.xScore = _score.x;
    _scoreboard.dataset.oScore = _score.o;
  };
  
  return public;
})();

var gameboard = (function () { 
  var public = {};
  
  var _gameboard;
  var _squaresHTML = '';
  
  public.createGameboard = function () {
  	document.body.insertAdjacentHTML('beforeend', '<div id="gameboard"></div>');
    _gameboard = document.querySelector('#gameboard');
    
    for (var i = 0; i < 9; i++) {
      _squaresHTML += '<div class="square" data-square="' + i + '"></div>';
    }
    
    _gameboard.insertAdjacentHTML('afterbegin', _squaresHTML);
    
    _gameboard.addEventListener('click', function (e) {
      if (e.target.id !== "gameboard" && !gameplay.getWinner() && !e.target.dataset.player) {
        e.target.dataset.player = _gameboard.dataset.message;
        
        gameplay.setMove(e.target.dataset.square, _gameboard.dataset.message);
        gameplay.checkForWin();

        if (AI.hasAI() && _gameboard.dataset.message === 'o') {
     ...