Pen Settings

CSS Base

Vendor Prefixing

Add External Stylesheets/Pens

Any URL's added here will be added as <link>s in order, and before the CSS in the editor. If you link to another Pen, it will include the CSS from that Pen. If the preprocessor matches, it will attempt to combine them before processing.

+ add another resource

You're using npm packages, so we've auto-selected Babel for you here, which we require to process imports and make it all work. If you need to use a different JavaScript preprocessor, remove the packages in the npm tab.

Add External Scripts/Pens

Any URL's added here will be added as <script>s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.

+ add another resource

Use npm Packages

We can make npm packages available for you to use in your JavaScript. We use webpack to prepare them and make them available to import. We'll also process your JavaScript with Babel.

⚠️ This feature can only be used by logged in users.

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.

HTML Settings

Here you can Sed posuere consectetur est at lobortis. Donec ullamcorper nulla non metus auctor fringilla. Maecenas sed diam eget risus varius blandit sit amet non magna. Donec id elit non mi porta gravida at eget metus. Praesent commodo cursus magna, vel scelerisque nisl consectetur et.

            
              This is the web worker for Align 4.

<!-- To see more, go here: https://codepen.io/lonekorean/pen/Gbwvc -->
            
          
!
            
              @import url(https://fonts.googleapis.com/css?family=Doppio+One);

body {
  margin: 20px;
  font: 60px/60px "Doppio One", sans-serif;
  color:#fff;
  background-color: #593f6b;
}
            
          
!
            
              // constants
var TOTAL_COLUMNS = 7;
var TOTAL_ROWS = 7;
var HUMAN_WIN_SCORE = -4;
var COMPUTER_WIN_SCORE = 4;
var NO_WIN_SCORE = 0;

// global variables
var currentGameState;

// game state object
var GameState = function(cloneGameState) {
  this.board = [];
  this.score = NO_WIN_SCORE;
  this.winningChips = undefined;
  
  // initialize an empty board
  for(var col = 0; col < TOTAL_COLUMNS; col++) {
    this.board[col] = [];
  }
  
  // clone from existing game state (if one was passed in)
  if(cloneGameState) {
    for (var col = 0; col < TOTAL_COLUMNS; col++) {
      for (var row = 0; row < cloneGameState.board[col].length; row++) {
        this.board[col][row] = cloneGameState.board[col][row];
      }
    }
    this.score = cloneGameState.score;
  }
}
GameState.prototype.makeMove = function(player, col) {
  var coords = undefined;
  var row = this.board[col].length;
  if (row < TOTAL_ROWS) {
    this.board[col][row] = player;
    this.setScore(player, col, row);
    coords = { col: col, row: row };
  }
  return coords;
}
GameState.prototype.isBoardFull = function() {
  for (var col = 0; col < TOTAL_COLUMNS; col++) {
    if (this.board[col].length < TOTAL_ROWS) {
      // found an unfilled column
      return false;
    }
  }
  return true;
}
GameState.prototype.setScore = function(player, col, row) {
  var isWin =
      this.checkRuns(player, col, row, 0, 1) || // vertical
      this.checkRuns(player, col, row, 1, 0) || // horizontal
      this.checkRuns(player, col, row, 1, 1) || // diagonal "/"
      this.checkRuns(player, col, row, 1, -1)   // diagonal "\"
  
  if(isWin) {
    this.score = (player === 1) ? HUMAN_WIN_SCORE : COMPUTER_WIN_SCORE;
  } else {
    this.score = NO_WIN_SCORE;
  }
}
GameState.prototype.checkRuns = function(player, col, row, colStep, rowStep) {
  var runCount = 0;
  
  // check from 3 chips before to 3 chips after the specified chip
  // this covers all possible runs of 4 chips that include the specified chip
  for (var step = -3; step <= 3; step++) {
    if (this.getPlayerForChipAt(col + step * colStep, row + step * rowStep) === player) {
      runCount++;
      if (runCount === 4) {
        // winning run, step backwards to find the chips that make up this run
        this.winningChips = [];
        for(var backstep = step; backstep >= step - 3; backstep--) {
          this.winningChips.push({
            col: col + backstep * colStep,
            row: row + backstep * rowStep
          });
        }
        return true;
      }
    } else {
      runCount = 0;
      if(step === 0) {
        // no room left for a win
        break;
      }
    }
  }
  
  // no winning run found
  return false;
}
GameState.prototype.getPlayerForChipAt = function(col, row) {
  var player = undefined;
  if (this.board[col] !== undefined && this.board[col][row] !== undefined) {
    player = this.board[col][row];
  }
  return player;
}
GameState.prototype.isWin = function() {
  return (this.score === HUMAN_WIN_SCORE || this.score === COMPUTER_WIN_SCORE);
}

// listen for messages from the main thread
self.addEventListener('message', function(e) {
  switch(e.data.messageType) {
    case 'reset':
      resetGame();
      break;
    case 'human-move':
      makeHumanMove(e.data.col);
      break;
    case 'computer-move':
      makeComputerMove(e.data.maxDepth);
      break;
  }
}, false);

function resetGame() {
  currentGameState = new GameState();
  
  self.postMessage({
    messageType: 'reset-done'
  });
}

function makeHumanMove(col) {
  // coords is undefined if the move is invalid (column is full)
  var coords = currentGameState.makeMove(1, col);
  var isWin = currentGameState.isWin();
  var winningChips = currentGameState.winningChips;
  var isBoardFull = currentGameState.isBoardFull();
  self.postMessage({
    messageType: 'human-move-done',
    coords: coords,
    isWin: isWin,
    winningChips: winningChips,
    isBoardFull: isBoardFull
  });
}

function makeComputerMove(maxDepth) {
  var col;
  var isWinImminent = false;
  var isLossImminent = false;
  for (var depth = 0; depth <= maxDepth; depth++) {
    var origin = new GameState(currentGameState);
    var isTopLevel = (depth === maxDepth);
    
    // fun recursive AI stuff kicks off here
    var tentativeCol = think(origin, 2, depth, isTopLevel);
    if (origin.score === HUMAN_WIN_SCORE) {
      // AI realizes it can lose, thinks all moves suck now, keep move picked at previous depth
      // this solves the "apathy" problem
      isLossImminent = true;
      break;
    } else if (origin.score === COMPUTER_WIN_SCORE) {
      // AI knows how to win, no need to think deeper, use this move
      // this solves the "cocky" problem
      col = tentativeCol;
      isWinImminent = true;
      break;
    } else {
      // go with this move, for now at least
      col = tentativeCol;
    }
  }
  
  var coords = currentGameState.makeMove(2, col);
  var isWin = currentGameState.isWin();
  var winningChips = currentGameState.winningChips;
  var isBoardFull = currentGameState.isBoardFull();
  self.postMessage({
    messageType: 'computer-move-done',
    coords: coords,
    isWin: isWin,
    winningChips: winningChips,
    isBoardFull: isBoardFull,
    isWinImminent: isWinImminent,
    isLossImminent: isLossImminent
  });
}

function think(node, player, recursionsRemaining, isTopLevel) {
  var scoreSet = false;
  var childNodes = [];

  // consider each column as a potential move
  for (var col = 0; col < TOTAL_COLUMNS; col++) {
    if(isTopLevel) {
      self.postMessage({
        messageType: 'progress',
        col: col
      });
    }
    
    // make sure column isn't already full
    var row = node.board[col].length;
    if (row < TOTAL_ROWS) {
      // create new child node to represent this potential move
      var childNode = new GameState(node);
      childNode.makeMove(player, col);
      childNodes[col] = childNode;
      
      if(!childNode.isWin() && recursionsRemaining > 0) {
        // no game stopping win and there are still recursions to make, think deeper
        var nextPlayer = (player === 1) ? 2 : 1;
        think(childNode, nextPlayer, recursionsRemaining - 1);
      }

      if (!scoreSet) {
        // no best score yet, just go with this one for now
        node.score = childNode.score;
        scoreSet = true;
      } else if (player === 1 && childNode.score < node.score) {
        // assume human will always pick the lowest scoring move (least favorable to computer)
        node.score = childNode.score;
      } else if (player === 2 && childNode.score > node.score) {
        // computer should always pick the highest scoring move (most favorable to computer)
        node.score = childNode.score;
      }
    }
  }

  // collect all moves tied for best move and randomly pick one
  var candidates = [];
  for (var col = 0; col < TOTAL_COLUMNS; col++) {
    if (childNodes[col] != undefined && childNodes[col].score === node.score) {
      candidates.push(col);
    }
  }
  var moveCol = candidates[Math.floor(Math.random() * candidates.length)];
  return moveCol;
}

            
          
!
999px
🕑 One or more of the npm packages you are using needs to be built. You're the first person to ever need it! We're building it right now and your preview will start updating again when it's ready.

Console