<div class="message">
  This pen is deprecated. Please check out the <a href="https://codepen.io/lonekorean/project/editor/ZGpqVX/" target="_blank">new project version here</a>.
</div>

<div class="wrapper">
  <div class="content">
    <div class="sidebar">
      <h1>Align 4</h1>
      <div class="panel dif">
        <h2>Difficulty</h2>
        <div class="dif-options">
          <div>
            <input id="dif1" type="radio" name="dif-options" value="1">
            <label for="dif1">1</label>
          </div>
          <div>
            <input id="dif2" type="radio" name="dif-options" value="2">
            <label for="dif2">2</label>
          </div>
          <div>
            <input id="dif3" type="radio" name="dif-options" value="3" checked>
            <label for="dif3">3</label>
          </div>
          <div>
            <input id="dif4" type="radio" name="dif-options" value="4">
            <label for="dif4">4</label>
          </div>
          <div>
            <input id="dif5" type="radio" name="dif-options" value="5">
            <label for="dif5">5</label>
          </div>
        </div>
        <div class="start">
          <button>Start Game</button>
        </div>
      </div>
      <div class="panel info">
        <h2></h2>
        <div class="blurb"></div>
        <div class="outlook"></div>
      </div>
    </div>
    <div class="board">
      <div class="lit-columns">
        <div></div>
        <div></div>
        <div></div>
        <div></div>
        <div></div>
        <div></div>
        <div></div>
      </div>
      <div class="lit-cells"></div>
      <div class="chips"></div>
      <div class="click-columns">
        <div></div>
        <div></div>
        <div></div>
        <div></div>
        <div></div>
        <div></div>
        <div></div>
      </div>
    </div>
  </div>
</div>
@import url(https://fonts.googleapis.com/css?family=Doppio+One);

.message {
  position: absolute;
  top: 0;
  left: 50%;
  transform: translateX(-50%);
  padding: 10px;
  border-bottom-left-radius: 10px;
  border-bottom-right-radius: 10px;
  background-color: #fff;
  font-family: "Doppio One", sans-serif;
  font-size: 20px;
}

html {
  display: table;
  width: 100%;
  height: 100%;
}
body {
  display: table-row;
  background: #000 radial-gradient(1000px 500px, #3f546b, #000);
}
.wrapper {
  display: table-cell;
  vertical-align: middle;
  text-align: center;
}
.content {
  display: inline-block;
  width: 668px;
  margin: 0 auto;
  padding: 50px 20px 10px;
}
.sidebar {
  float: left;
  margin-right: 20px;
  width: 220px;
  text-align: left;
  font-family: "Doppio One", sans-serif;
  color: #ccc;
}
h1, h2 {
  color: #fff;
  margin: 0;
  font-weight: normal;
}
h1 {
  height: 68px;
  line-height: 68px;
  font-size: 40px;
  text-align: right;
}
h2 {
  font-size: 18px;
}
.panel {
  padding: 12px;
  margin-bottom: 20px;
  background-color: rgba(255, 255, 255, 0.1);
  border-radius: 8px;
}
.dif-options {
  clear: both;
  overflow: hidden;
  margin: 20px -7px 0;
}
.dif-options div {
  float: left;
  width: 20%;
}
.dif-options input {
  display: none;
}
.dif-options label {
  display: block;
  margin: 0 auto;
  width: 24px;
  height: 24px;
  background-color: #666;
  border: solid 2px #ccc;
  border-radius: 8px;
  color: #999;
  text-align: center;
  line-height: 24px;
  cursor: pointer;
}
.dif-options input:checked + label {
  color: #fff;
  background-color: #593f6b;
  border-color: #fff;
  cursor: default;
}
.freeze .dif-options input:not(:checked) + label {
  font-size: 0;
  margin: 7px auto;
  width: 10px;
  height: 10px;
  border-radius: 4px;
  color: transparent;
  line-height: 10px;
  cursor: default;
  transition: .2s;
}
.start {
  margin-top: 20px;
}
.freeze .start {
  display: none;
}
.start button {
  display: block;
  width: 100%;
  padding: 2px 12px 4px;
  font-family: "Doppio One", sans-serif;
  font-size: 24px;
  border: solid 2px #ccc;
  border-radius: 8px;
  background-color: #593f6b;
  color: #fff;
  cursor: pointer;
}
.start button:focus {
  outline: none;
}
.info div {
  margin-top: 10px;
}
.board {
  position: relative;
  float: left;
  width: 428px;
  height: 428px;
  margin-top: 68px;
  background-image: url(https://s3-us-west-2.amazonaws.com/s.cdpn.io/77020/board.png);
  box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.4);
}
.lit-columns, .lit-cells, .chips, .click-columns {
  position: absolute;
  width: 428px;
  height: 428px;  
}
.lit-columns div {
  float: left;
  width: 60px;
  height: 426px;
  margin: 1px 0 1px 1px;
  transition: background-color 0.1s;
}
.lit-columns .lit {
  background-color: rgba(255, 255, 255, 0.1);
  transition: background-color 0.1s;
}
.lit-cells div {
  position: absolute;
  width: 60px;
  height: 60px;
  background-color: rgba(255, 255, 255, 0.3);
}
.chips div {
  position: absolute;
  width: 60px;
  height: 60px;
  backface-visibility: hidden;
}
.chips .p1 {
  background-image: url(https://s3-us-west-2.amazonaws.com/s.cdpn.io/77020/p1-chip.png);
}
.chips .p2 {
  background-image: url(https://s3-us-west-2.amazonaws.com/s.cdpn.io/77020/p2-chip.png);
}
.chips .cursor {
  bottom: 428px;
  transition: left 0.1s ease-out;
}
.chips .dropped {
  transition: bottom ease-in;
  animation: bounce 0.3s;
}
.click-columns div {
  float: left;
  width: 61px;
  height: 428px;
}
.click-columns div:first-child {
  padding-left: 1px;
}
.click-columns .hover {
  cursor: pointer;
}

@keyframes bounce {
  0% { animation-timing-function: ease-out; transform: translateY(0); }
  30% { animation-timing-function: ease-in; transform: translateY(-40px); }
  60% { animation-timing-function: ease-out; transform: translateY(0); }
  80% { animation-timing-function: ease-in; transform: translateY(-10px); }
  100% { animation-timing-function: ease-out; transform: translateY(0); }
}
// constants
var WEB_WORKER_URL = 'https://s.codepen.io/lonekorean/pen/KnaJf.js';
var BLURBS = {
  'start': {
    header: 'Get Ready',
    blurb: 'Select your difficulty and start the game.'
  },
  'p1-turn': {
    header: 'Your Turn',
    blurb: 'Click on the board to drop your chip.'
  },
  'p2-turn': {
    header: 'Computer\'s Turn',
    blurb: 'The computer is trying to find the best way to make you lose.'
  },
  'p1-win': {
    header: 'You Win',
    blurb: 'You are a winner. Remember this moment. Carry it with you, forever.'
  },
  'p2-win': {
    header: 'Computer Wins',
    blurb: 'Try again when you\'re done wiping your tears of shame.'
  },
  'tie': {
    header: 'Tie',
    blurb: 'Everyone\'s a winner! Or loser. Depends on how you look at it.'
  }
};
var OUTLOOKS = {
  'win-imminent': 'Uh oh, computer is feeling saucy!',
  'loss-imminent': 'Computer is unsure. Now\'s your chance!'
};

// global variables
var worker;
var currentGameState;

// document ready
$(function() {
  $('.start button').on('click', startGame);
  setBlurb('start');
  setOutlook();
  
  // create worker
  worker = new Worker(WEB_WORKER_URL);
  worker.addEventListener('message', function(e) {
    switch(e.data.messageType) {
      case 'reset-done':
        startHumanTurn();
        break;
      case 'human-move-done':
        endHumanTurn(e.data.coords, e.data.isWin, e.data.winningChips, e.data.isBoardFull);
        break;
      case 'progress':
        updateComputerTurn(e.data.col);
        break;
      case 'computer-move-done':
        endComputerTurn(e.data.coords, e.data.isWin, e.data.winningChips, e.data.isBoardFull,
          e.data.isWinImminent, e.data.isLossImminent);
        break;
    }
  }, false);
});

function setBlurb(key) {
  $('.info h2').text(BLURBS[key].header);
  $('.info .blurb').text(BLURBS[key].blurb);
}

function setOutlook(key) {
  var $outlook = $('.info .outlook');
  if(key) {
    $outlook
      .text(OUTLOOKS[key])
      .show();
  } else {
    $outlook.hide();
  }
}

function startGame() {
  $('.dif').addClass('freeze');
  $('.dif input').prop('disabled', true);
  $('.lit-cells, .chips').empty();

  worker.postMessage({
    messageType: 'reset',
  });
}

function startHumanTurn() {
  setBlurb('p1-turn');
  $('.click-columns div').addClass('hover');
  
  // if mouse is already over a column, show cursor chip there
  var col = $('.click-columns div:hover').index();
  if(col !== -1) {
    createCursorChip(1, col);
  }
  
  $('.click-columns')
    .on('mouseenter', function() {
      var col = $('.click-columns div:hover').index();
      createCursorChip(1, col);
    })
    .on('mouseleave', function() {
      destroyCursorChip();
    });
  
  $('.click-columns div')
    .on('mouseenter', function() {
      var col = $(this).index();
      moveCursorChip(col);
    })
    .on('click', function() {
      $('.click-columns, .click-columns div').off();
      
      var col = $(this).index();
      worker.postMessage({
        messageType: 'human-move',
        col: col
      });
    });  
}

function endHumanTurn(coords, isWin, winningChips, isBoardFull) {
  $('.click-columns div').removeClass('hover');
  if(!coords) {
    // column was full, human goes again
    startHumanTurn();    
  } else {
    dropCursorChip(coords.row, function() {
      if(isWin) {
        endGame('p1-win', winningChips);
      } else if(isBoardFull) {
        endGame('tie');
      } else {
        // pass turn to computer
        startComputerTurn();
      }
    });
  }
}

function startComputerTurn() {
  setBlurb('p2-turn');
  
  // computer's cursor chip starts far left and moves right as it thinks
  createCursorChip(2, 0);
  
  var maxDepth = parseInt($('input[name=dif-options]:checked').val(), 10) + 1;
  worker.postMessage({
    messageType: 'computer-move',
    maxDepth: maxDepth
  });
}

function updateComputerTurn(col) {
  moveCursorChip(col);
}

function endComputerTurn(coords, isWin, winningChips, isBoardFull, isWinImminent, isLossImminent) {
  moveCursorChip(coords.col, function() {
    dropCursorChip(coords.row, function() {
      if (isWin) {
        endGame('p2-win', winningChips);
      } else if (isBoardFull) {
        endGame('tie');
      } else {
        if(isWinImminent) {
          setOutlook('win-imminent');
        } else if (isLossImminent) {
          setOutlook('loss-imminent');
        } else {
          setOutlook();
        }
        
        // pass turn to human
        startHumanTurn();
      }
    });
  });
}

function endGame(blurbKey, winningChips) {
  $('.dif').removeClass('freeze');
  $('.dif input').prop('disabled', false);
  setBlurb(blurbKey);
  setOutlook();
  
  if(winningChips) {
    // not a tie, highlight the chips in the winning run
    for(var i = 0; i < winningChips.length; i++) {
      createLitCell(winningChips[i].col, winningChips[i].row);
    }
  }
}

function createLitCell(col, row) {
  $('<div>')
  .css({
    'left': indexToPixels(col),
    'bottom': indexToPixels(row)
  })
  .appendTo('.lit-cells');
}

function createCursorChip(player, col) {
  var playerClass = 'p' + player;
  $('<div>', { 'class': 'cursor ' + playerClass })
    .css('left', indexToPixels(col))
    .appendTo('.chips');
  
  // also highlight column
  $('.lit-columns div').eq(col).addClass('lit');
}

function destroyCursorChip() {
  $('.chips .cursor').remove();
  $('.lit-columns .lit').removeClass('lit');
}

function moveCursorChip(col, callback) {
  $('.chips .cursor').css('left', indexToPixels(col));
  $('.lit-columns .lit').removeClass('lit');
  $('.lit-columns div').eq(col).addClass('lit');
  
  // callback is only used when the computer is about to drop a chip
  // give it a slight delay for visual interest
  setTimeout(callback, 300);
}

function dropCursorChip(row, callback) {
  // speed of animation depends on how far the chip has to drop
  var ms = (7 - row) * 40;
  var duration = (ms / 1000) + 's';
  
  $('.chips .cursor')
    .removeClass('cursor')
    .css({
      'bottom': indexToPixels(row),
      '-webkit-transition-duration': duration, 'transition-duration': duration,
      '-webkit-animation-delay': duration, 'animation-delay': duration
    })
    .addClass('dropped');
  
  $('.lit-columns .lit').removeClass('lit');
  setTimeout(callback, ms);
}

function indexToPixels(index) {
  return (index * 61 + 1) + 'px';
}

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. //cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js