// Javascript Connect Four with AI - ALPHA version
// ©2005 John Shirrell
// CSCI 490B Project
// Programmed entirely in an HTML/Script editor I programmed myself, just for fun

// Game settings - change to weird values at own risk!
// Default values are: false, 7, 6, 1.

var debugMenu = false;			// Show debug menu, true or false - show anytime with ?debug
var numCols = 7;				// Number of columns, integer
var numRows = 6;				// Number of rows, integer
var firstMove = 1;			// -1 = Black, 1 = Red

// These variables initialize the game and should never be changed.

// Pre-load images

var rb0 = new Image();
rb0.src = "0.png";

var rb1 = new Image();
rb1.src = "1.png";

var rbn1 = new Image();
rbn1.src = "-1.png";

var inPlay = false;			// Game is not in play until started 

var turn = firstMove;			// First move goes to first player

var board = new Array(numRows);	// Sets up board matrix

var cp1 = 0;				// Computer player 1, always chooses red
var cpn1 = 0;				// Computer player -1, always chooses black

var bestMove = -1;			// Storage for best move in AI search

var cons = "";				// Debug menu console text. Used for testing.

// Variables below here are settable by the Debug Menu. Changing these changes default.

var aniPieces = true;			// Piece animation

// AI Skill variables

var cpn1maxDepth = 3;
var cpn1forWin = 1000000;
var cpn1forLoss = -100000;
var cpn1forDraw = 0;
var cpn1forWhammy = 99;
var cpn1forRoads = 6;
var cpn1forBlocking = 2;
var cpn1forBlocked = -2;
var cpn1forOE = 50;
var cpn1immed = 2;
var cp1maxDepth = 3;
var cp1forWin = 100000;
var cp1forLoss = -100000;
var cp1forDraw = 0;
var cp1forWhammy = 99;
var cp1forRoads = 6;
var cp1forBlocking = 2;
var cp1forBlocked = -2;
var cp1forOE = 50;
var cp1immed = 2;

function startGame() {
   inPlay = true;				// Start game

   turn = firstMove;			// First move goes to first player

   for (var ainit = 0; ainit < numRows; ainit++) {
      board[ainit] = new Array(numCols);	// Loop sets up y dimension and inits to 0
      for (var n = 0; n < numCols; n++) {
         board[ainit][n] = 0;			// For the sake of this game, y measures depth from top row.
         eval("document.images[\"i"+ainit+"x"+n+"\"].src = \"0.png\";");
      }
   }

   if (document.forms[0].cpred.checked) { cp1 = 1; } else { cp1 = 0; }
   if (document.forms[0].cpblk.checked) { cpn1 = -1; } else { cpn1 = 0; }

   if (cp1 == firstMove || cpn1 == firstMove) {
      compMove(firstMove);
   }
   
   document.forms[0].cpred.disabled = true;
   document.forms[0].cpblk.disabled = true;
   document.forms[0].sbut.disabled = true;
}

function click(column) {
  // Drop current player's piece into column
  // Step 1: Check to see whether game is in play
  if (inPlay) {
     // Step 2: Do not allow a click while it is computer's turn
     if (turn != cp1 && turn != cpn1) {
        // Step 3: Check to see whether move is legal (column cannot be full)
        // To accomplish this we assume gravity is working and check only top index
        if (board[0][column]) {
           // Will evaluate as true if space occupied, thus illegal move
           alert("This column is full. No further moves permitted here.");
        }
        else {
           // Legal move.
           // Step 4: Insert piece
           insertPiece(column);
           // Step 5: Check for winning status
           var stat = checkWin(board);
           if (stat) {
             alertState(stat);
           }
           // Step 6: Swap turns
           turn *= -1;
           // Step 7: Check for computer turn
           if (inPlay && (turn == cp1 || turn == cpn1)) {
		  setTimeout('compMove();', 1);	// Perform computer move, timer to prevent piece delay
           }
        }
     }
     else {
        // Trying to move out of turn
        alert("It is not your turn. Please wait until computer player finishes.");
     }
  }
  else {
    // Start new game
    startGame();
  }
}

function insertPiece(column) {
  for (var top = numRows - 1; top >= 0; top--) {	// Start at bottom and work way up to find viable value for top
     if (board[top][column] == 0) {
        // Update board
        board[top][column] = turn;

        // Update screen
        eval("document.images[\"i"+top+"x"+column+"\"].src = \""+turn+".png\";");

        if (aniPieces && (turn == cp1 || turn == cpn1)) {
	     // Animate piece
	     setTimeout("document.images[\"i"+top+"x"+column+"\"].src = \"0.png\";", 100);
	     setTimeout("document.images[\"i"+top+"x"+column+"\"].src = \""+turn+".png\";", 200);
	     setTimeout("document.images[\"i"+top+"x"+column+"\"].src = \"0.png\";", 300);
	     setTimeout("document.images[\"i"+top+"x"+column+"\"].src = \""+turn+".png\";", 400);
	  }
	  // Once a home is found, break
	  break;
     }
  }
}

function checkWin(boardMap) {
   // This function will look at the board for a winning pattern for either player and return
   // 0 = no win, 1 = red wins, -1 = black wins, -2 = a draw.
   var boardState = 0;						// Default return = 0

   // First, scan for draw, since that is easy
   // Assume that a full board draw exists until proven false (one-step denial if [0][0] clear)
   var foundHole = false;

   for (var holeLook = 0; holeLook < numCols; holeLook++) {
      if (!boardMap[0][holeLook]) {
        // Top space is not occupied, so no draw
        foundHole = true;
	  break;
      }
   }
   if (!foundHole) {
      // Draw state exists, no hole found. Report draw on board State.
      // (It is still necessary to check for a win, in case the last move created 4-in-a-row.)
      boardState = -2;
   }

   // Test for four red in a row.
   var has4Red = testVHD(1,boardMap);
   if (has4Red) {
      boardState = 1;
   }
   else {
      var has4Black = testVHD(-1,boardMap);
      if (has4Black) {
         boardState = -1;
      }
   }

   // All testing finished, return board state.
   return boardState;
}

function testVHD(who, boardMap) {
   // Test vertical, horizontal, diagonal cases for 4-in-a-row.
   // This will check for a 4-in-a-row in favor of the player specified in parameter who.
   // All documentation will refer to the case of testing for red 4-in-a-row.
   // However, this function does the job for both red and black.

   var hasWin = false;				// Changes to true if winning pattern found

   // First test: Vertical pattern test
   // It is not necessary to check last three rows unless red occupies fourth from last spot.
   for (var vscan = 0; vscan < numRows - 3; vscan++) {
      for (var vcol = 0; vcol < numCols; vcol++) {
         if (boardMap[vscan][vcol] == who) {
            if (boardMap[vscan+1][vcol] == who && boardMap[vscan+2][vcol] == who && boardMap[vscan+3][vcol] == who) {

               // Winning pattern found - vertical
               hasWin = true;			// Can only break out of column loop. 
               break;				// Comparison would be worse than no break for outer loop.
		}
	   }
      }
   }

   // Second test: Horizontal pattern test
   // It is not necessary to check last three cols unless red occupies fourth from last spot.
   for (var hscan = 0; hscan < numCols - 3; hscan++) {
      for (var hrow = 0; hrow < numRows; hrow++) {
         if (boardMap[hrow][hscan] == who) {
            if (boardMap[hrow][hscan+1] == who && boardMap[hrow][hscan+2] == who && boardMap[hrow][hscan+3] == who) {

               // Winning pattern found - horizontal
               hasWin = true;			// Can only break out of row loop. 
               break;				// Comparison would be worse than no break for outer loop.
		}
	   }
      }
   }

   // Third test: Forward Slash "/" Diagonal pattern test
   // It is not necessary to check last three cols unless red occupies fourth from last spot.
   // Start in row 3, because pattern cannot begin above that row.
   for (var fscan = 0; fscan < numCols - 3; fscan++) {
      for (var frow = 3; frow < numRows; frow++) {
         if (boardMap[frow][fscan] == who) {
            if (boardMap[frow-1][fscan+1] == who && boardMap[frow-2][fscan+2] == who && boardMap[frow-3][fscan+3] == who) {

               // Winning pattern found - forward slash diagonal
               hasWin = true;			// Can only break out of row loop. 
               break;				// Comparison would be worse than no break for outer loop.
		}
	   }
      }
   }

   // Fourth test: Back Slash "\" Diagonal pattern test
   // It is not necessary to check last three cols unless red occupies fourth from last spot.
   // Stop 3 rows short, because pattern cannot end below that row.
   for (var bscan = 0; bscan < numCols - 3; bscan++) {
      for (var brow = 0; brow < numRows - 3; brow++) {
         if (boardMap[brow][bscan] == who) {
            if (boardMap[brow+1][bscan+1] == who && boardMap[brow+2][bscan+2] == who && boardMap[brow+3][bscan+3] == who) {

               // Winning pattern found - back slash diagonal
               hasWin = true;			// Can only break out of row loop. 
               break;				// Comparison would be worse than no break for outer loop.
		}
	   }
      }
   }


   return hasWin;
}

function alertState(status) {
   // 1 = red wins, -1 = black wins, -2 = a draw.
   // Alerts status, ends game, unlocks checkboxes.

   if (status == -2) {
      alert("Game ends in a draw!");
   }
   else if (status == -1) {
      alert("Black wins!")
   }
   else {
      alert("Red wins!")
   }
   inPlay = false;
   document.forms[0].cpred.disabled = false;
   document.forms[0].cpblk.disabled = false;
   document.forms[0].sbut.disabled = false;
}

// Page writer code begins

document.write("<CENTER><H1>Co"+"nnect F"+"our</H1>"); // Title
// Let's not make it easy to find + trim this plug from the script
document.write("By Jo"+"hn Shir"+"rell - Re"+"sum"+"e@"+"Jo"+"hnS"+"hir"+"re"+"ll."+"co"+"m<P>");
document.write("<FORM><INPUT TYPE=\"button\" VALUE=\"Start Game\" NAME=\"sbut\" onClick=\"startGame();\"><P>");
document.write("Computer plays: <INPUT TYPE=\"checkbox\" CHECKED NAME=\"cpblk\"> Black");
document.write("<INPUT TYPE=\"checkbox\" NAME=\"cpred\"> Red &bull; Animated pieces: <INPUT TYPE=\"checkbox\" ");
if (aniPieces) { document.write("CHECKED "); }	// Represent default value
document.write("NAME=\"cbanim\" onClick=\"updAnim();\"> Yes</FORM>");
document.write("<TABLE BORDER=\"1\"><TR><TD>");
if (debugMenu || document.location.search == "?debug") {
   debugMenu = true;			// Set debugMenu in case of the latter
   drawDebug();
   document.write("</TD><TD>");
}
drawBoard();
if (debugMenu || document.location.search == "?debug") {
   document.write("</TD></TR></TABLE>");
}
document.write("</CENTER>");

// Page writer code ends

function drawDebug() {
   // Draws debug menu
   document.write("<CENTER>Debug Menu<P>");
   document.write("<FORM><TABLE BORDER=\"1\"><TR><TD>Variable</TD><TD>CP Black</TD><TD>CP Red</TD></TR>");
   document.write("<TR><TD>MaxDepth</TD><TD><INPUT TYPE=\"text\" NAME=\"txtcpn1maxDepth\" VALUE=\""+cpn1maxDepth+"\" SIZE=\"3\"></TD>");
   document.write("<TD><INPUT TYPE=\"text\" NAME=\"txtcp1maxDepth\" VALUE=\""+cp1maxDepth+"\" SIZE=\"3\"></TD></TR>");
   document.write("<TR><TD>Win</TD><TD><INPUT TYPE=\"text\" NAME=\"txtcpn1forWin\" VALUE=\""+cpn1forWin+"\" SIZE=\"3\"></TD>");
   document.write("<TD><INPUT TYPE=\"text\" NAME=\"txtcp1forWin\" VALUE=\""+cp1forWin+"\" SIZE=\"3\"></TD></TR>");
   document.write("<TR><TD>Loss</TD><TD><INPUT TYPE=\"text\" NAME=\"txtcpn1forLoss\" VALUE=\""+cpn1forLoss+"\" SIZE=\"3\"></TD>");
   document.write("<TD><INPUT TYPE=\"text\" NAME=\"txtcp1forLoss\" VALUE=\""+cp1forLoss+"\" SIZE=\"3\"></TD></TR>");
   document.write("<TR><TD>Draw</TD><TD><INPUT TYPE=\"text\" NAME=\"txtcpn1forDraw\" VALUE=\""+cpn1forDraw+"\" SIZE=\"3\"></TD>");
   document.write("<TD><INPUT TYPE=\"text\" NAME=\"txtcp1forDraw\" VALUE=\""+cp1forDraw+"\" SIZE=\"3\"></TD></TR>");
   document.write("<TR><TD>Whammy</TD><TD><INPUT TYPE=\"text\" NAME=\"txtcpn1forWhammy\" VALUE=\""+cpn1forWhammy+"\" SIZE=\"3\"></TD>");
   document.write("<TD><INPUT TYPE=\"text\" NAME=\"txtcp1forWhammy\" VALUE=\""+cp1forWhammy+"\" SIZE=\"3\"></TD></TR>");
   document.write("<TR><TD>ForBlocking</TD><TD><INPUT TYPE=\"text\" NAME=\"txtcpn1forBlocking\" VALUE=\""+cpn1forBlocking+"\" SIZE=\"3\"></TD>");
   document.write("<TD><INPUT TYPE=\"text\" NAME=\"txtcp1forBlocking\" VALUE=\""+cp1forBlocking+"\" SIZE=\"3\"></TD></TR>");
   document.write("<TR><TD>ForBlocked</TD><TD><INPUT TYPE=\"text\" NAME=\"txtcpn1forBlocked\" VALUE=\""+cpn1forBlocked+"\" SIZE=\"3\"></TD>");
   document.write("<TD><INPUT TYPE=\"text\" NAME=\"txtcp1forBlocked\" VALUE=\""+cp1forBlocked+"\" SIZE=\"3\"></TD></TR>");
   document.write("<TR><TD>Roads</TD><TD><INPUT TYPE=\"text\" NAME=\"txtcpn1forRoads\" VALUE=\""+cpn1forRoads+"\" SIZE=\"3\"></TD>");
   document.write("<TD><INPUT TYPE=\"text\" NAME=\"txtcp1forRoads\" VALUE=\""+cp1forRoads+"\" SIZE=\"3\"></TD></TR>");
   document.write("<TR><TD>Odd+Even Wins</TD><TD><INPUT TYPE=\"text\" NAME=\"txtcpn1forOE\" VALUE=\""+cpn1forOE+"\" SIZE=\"3\"></TD>");
   document.write("<TD><INPUT TYPE=\"text\" NAME=\"txtcp1forOE\" VALUE=\""+cp1forOE+"\" SIZE=\"3\"></TD></TR>");
   document.write("<TR><TD>Immediate Win</TD><TD><INPUT TYPE=\"text\" NAME=\"txtcpn1immed\" VALUE=\""+cpn1immed+"\" SIZE=\"3\"></TD>");
   document.write("<TD><INPUT TYPE=\"text\" NAME=\"txtcp1immed\" VALUE=\""+cp1immed+"\" SIZE=\"3\"></TD></TR>");
   document.write("<TR><TD COLSPAN=\"3\"><CENTER><INPUT TYPE=\"button\" VALUE=\"Update\" onClick=\"updtAI();\"></CENTER></TD></TR></TABLE>");
   document.write("<INPUT TYPE=\"checkbox\" NAME=\"showEval\"> Show Evaluations<BR><TEXTAREA NAME=\"console\" LOCKED></TEXTAREA></FORM></CENTER>");
}

function drawBoard() {
  for (var r = 0; r < numRows; r++) {		// Begin row
     for (var c = 0; c < numCols; c++) {	// Begin column
	  document.write("<A STYLE=\"cursor:hand;\" HREF=\"javascript:click("+c+");\">");
        document.write("<IMG ID=\"i"+r+"x"+c+"\" SRC=\"0.png\" WIDTH=\"70\" HEIGHT=\"70\" BORDER=\"0\"></A>");
     }
     document.write("<BR>");			// Break line at end of row
  }
}

// ALL AI is BELOW this point ****************************************************

function updtAI() {
   // Updates values from form for AI. Can be done anytime.
   cpn1maxDepth = Math.floor(document.forms[1].txtcpn1maxDepth.value);
   cpn1forWin = Math.floor(document.forms[1].txtcpn1forWin.value);
   cpn1forLoss = Math.floor(document.forms[1].txtcpn1forLoss.value);
   cpn1forDraw = Math.floor(document.forms[1].txtcpn1forDraw.value);
   cpn1forWhammy = Math.floor(document.forms[1].txtcpn1forWhammy.value);
   cpn1forRoads = Math.floor(document.forms[1].txtcpn1forRoads.value);
   cpn1forBlocking = Math.floor(document.forms[1].txtcpn1forBlocking.value);
   cpn1forBlocked = Math.floor(document.forms[1].txtcpn1forBlocked.value);
   cpn1forOE = Math.floor(document.forms[1].txtcpn1forOE.value);
   cpn1immed = Math.floor(document.forms[1].txtcpn1immed.value);
   cp1maxDepth = Math.floor(document.forms[1].txtcp1maxDepth.value);
   cp1forWin = Math.floor(document.forms[1].txtcp1forWin.value);
   cp1forLoss = Math.floor(document.forms[1].txtcp1forLoss.value);
   cp1forDraw = Math.floor(document.forms[1].txtcp1forDraw.value);
   cp1forWhammy = Math.floor(document.forms[1].txtcp1forWhammy.value);
   cp1forRoads = Math.floor(document.forms[1].txtcp1forRoads.value);
   cp1forBlocking = Math.floor(document.forms[1].txtcp1forBlocking.value);
   cp1forBlocked = Math.floor(document.forms[1].txtcp1forBlocked.value);
   cp1forOE = Math.floor(document.forms[1].txtcp1forOE.value);
   cp1immed = Math.floor(document.forms[1].txtcp1immed.value);

   if (cpn1maxDepth > 4) {
	alert("Maximum maxDepth is 4. Higher will hang the system.");
      cpn1maxDepth = 4;
   }
   if (cp1maxDepth > 4) {
	alert("Maximum maxDepth is 4. Higher will hang the system.");
      cp1maxDepth = 4;
   }
   if (!cpn1immed > 0) {
	alert("Surely you don't want the AI to ignore a win/loss?\nImmediate multiplier must be > 0.");
      cpn1immed = 1;
   }
   if (!cp1immed > 0) {
	alert("Surely you don't want the AI to ignore a win/loss?\nImmediate multiplier must be > 0.");
      cp1immed = 1;
   }
}

function updAnim() {
   // Updates animation option
   if (document.forms[0].cbanim.checked) {
	aniPieces = true;
   }
   else {
	aniPieces = false;
   }
}

function compMove() {
   // Start a recursive alpha beta search for the best move

   bestMove = -1;
   var target = 0;

   if (debugMenu && document.forms[1].showEval.checked) {
	document.forms[1].console.value = "";
	cons = "·";
   }

   target = abSearch(0, -9999999, 9999999, board);

   if (debugMenu && document.forms[1].showEval.checked) {
	document.forms[1].console.value = cons;
   }

   if (bestMove == -1) {
      alert("Unknown AI error - no legal move found.\n"+target)
   }
   else {
      compPut(bestMove);
      if (cp1 && cpn1) {
         // compMove controls turns in a PC vs. PC game.
         if (inPlay) {
            if (aniPieces) {
      	   setTimeout('compMove();', 1000);	// Perform computer move, timer to allow piece animation
		}
		else {
   		   setTimeout('compMove();', 1);	// Perform computer move, timer is still preferred over recursion
		}
	   }
	}
   }
}

function abSearch(depth, alpha, beta, boardIn) {
   // This is the alpha beta search traversal. It will report the score for the best/worst move.
   // Scoring may vary based on current player. boardIn is a copy of the board for this instantiation.
   // checkWin: 0 = no win, 1 = red wins, -1 = black wins, -2 = a draw.

   // Pre-condition: Existing variable bestMove, which will be given the min/max move for level.

   var maxDepth = cpn1maxDepth;
   if (turn == cp1) { maxDepth = cp1maxDepth; }

   var bstate = checkWin(boardIn);

   var returnval = 0;				// Return value

   var futBoard = new Array(numRows);	// Sets up future board matrix
   var newval;  					// For use as value storage for newest test
   var vturn = turn;				// Keeps track of turn being simulated
   if (isMin(depth)) {
	// Simulate opponent
	vturn = turn * -1;
   }

   if (depth < maxDepth) {
      // Maximize or minimize this level based on child nodes
	// Check for win at every level - do not recurse past a winning pattern
      if (bstate) {
         // Immediate win occurs - multiply win value by immediate win multiplier
         if (turn == cpn1) {
            returnval = (cpn1immed * heur(bstate, turn));
         }
	   else {
            returnval = (cp1immed * heur(bstate, turn));
         }
      }
      else {
         // Next: Loop through each possible move

         for (var move = 0; move < numCols; move++) {
            // Loop through with move being column to try.

            // Halt if move has no chance of being selected (alpha beta pruning)
            if (alpha > beta) { break; }

            // Do not even consider returning an illegal move - test here
            if (!boardIn[0][move]) {

               // There is an empty space, so this is a legal move

		   // Perform move on copy of board
		   for (fbr = 0; fbr < numRows; fbr++) {
		      futBoard[fbr] = new Array(numCols);
			for (fbc = 0; fbc < numCols; fbc++) {
	   		   futBoard[fbr][fbc] = boardIn[fbr][fbc];
			}
		   }
		   futBoard = virtualInsert(move, futBoard, vturn);

		   if (isMin(depth)) {
			newval = abSearch(depth+1, -9999999, beta, futBoard);
		   }
		   else {
			newval = abSearch(depth+1, alpha, 9999999, futBoard);
		   }
               if (depth == 0 && debugMenu) {
			// Add evaluation data to screen
			cons += newval+"·";
		   }

               // Alpha beta update and storing of new best move at root if applicable
               if (isMin(depth)) {
                  // Level is minimizing
                  beta = Math.min(beta, newval);
               }
		   else {
			alpha = Math.max(alpha, newval);	// Setting new max, if newval is max
			if (alpha == newval && !depth) {
			   // alpha was the max and we are at the root of the game tree picking a move.
			   // Store the new best move for computer to make. If a higher max comes along this will be
			   // modified again:
			   bestMove = move;
			}
		   }
            }
         }
	   if (isMin(depth)) {
		returnval = beta;
	   }
	   else {
		returnval = alpha;
	   }
      }
   }
   else {
      // Maxdepth reached, start scoring
      returnval = heur(bstate);		// Return value, if any, generated by winning
	returnval += heurEval(boardIn);	// Add heuristic evaluation to weight moves
   }
   return returnval;
}

function isMin(oddnum) {
   // This is a simple odd number test. If the computer's first move is zero, that move is maximizing.
   // Opponent's move, one, is minimizing. This holds true for any odd depth.

   return ((oddnum / 2) != (Math.floor(oddnum / 2)));
}

function compPut(column) {
   // Step 1: Insert piece
   insertPiece(column);
   // Step 2: Check for winning status
   var stat = checkWin(board);
   if (stat) {
      alertState(stat);
   }
   // Step 3: Swap turns
   turn *= -1;
}

function virtualInsert(column, matrix, vturn) {
   // Does not modify game board, instead modifies a copy matrix and returns updated matrix

   for (var top = numRows - 1; top >= 0; top--) {	// Start at bottom and work way up
      if (matrix[top][column] == 0) {
         // Update board
         matrix[top][column] = vturn;

	   // Once a home is found, break
	   break;
      }
   }
   return matrix;
}

// All heuristic scoring is BELOW this point ****************************************************

function heur(bstate) {
   // This function provides a value for wins, losses, and draws.
   // Do not confuse with heurEval which evaluates the position of pieces on the board for maxDepth.

   var rval = 0;			// Default = 0, which will be returned in the event this is not a winning state

   var forWin = cpn1forWin;
   var forLoss = cpn1forLoss;
   var forDraw = cpn1forDraw;

   if (turn == cp1) {
      forWin = cp1forWin;
      forLoss = cp1forLoss;
      forDraw = cp1forDraw;
   }
   if (bstate == -2) { rval = forDraw; }
   else if (bstate == turn) { rval = forWin; }
   else if (bstate == (-1 * turn)) { rval = forLoss; }

   return rval;
}

function heurEval(boardData) {
   // Scores the boardData parameter based on its value calculated by all heuristics

   var heurtotal = 0;		// This will contain a score based on the layout of boardData

   // First heuristic: Double Whammy
   heurtotal += doubleWhammy(boardData);

   // Second heuristic: Blocking
   heurtotal += isBlocked(boardData);

   // Third heuristic: Roads To Opportunity
   heurtotal += roadsToOpportunity(boardData);

   // Fourth heuristic: Odd/Even Wins
   heurtotal += hasOddEvenWins(boardData);

   return heurtotal;
}

function doubleWhammy(boardH1) {
   // Double Whammy is the killer strategy in Connect Four. If an AI is going to have a weakness,
   // it is probably weak to a Double Whammy. This heuristic returns an exponential value based on
   // the number of winning patterns that are available in one move; positive for computer,
   // negative for opponent. Value is calculated based on this formula: C ^ wp.
   // Example puzzle:  Contains two wins for o, one for x.        o
   // This heuristic disregards which player moves next for       oo o
   // simplicity. However, in this situation, the player who      xx x
   // moves next has a sure win. Avoid a suicide play.            xxxo

   // This illustrates the need for win/loss testing to greatly take precedence even over
   // multiple-win patterns, so keep the value for a win higher than even, say, the cubed power of C.
   // Three winning patterns will do you no good if it is the opponent's turn and he wins.

   var numRed = 0;		// Simply number of moves Red can make to connect four.
   var numBlk = 0;		// Same for black.
   var numPM = 0;			// Number of matches for current value of pm
   var retval = 0;		// Return value.
   var sumNotOp = 0;		// Sum of pieces in line of four that do not belong to opponent

   // This test uses a modified form of the checkWin search. In red's case, instead of looking for four
   // red pieces, we look for three red pieces and one hole.

   // The test operations will compare (cell value * pm) >= 0 for match of holes or pm pieces.
   // This way, if pm is -1 (black), a piece that is 1 (red) will be < 0 and fail.
   // Alternatively, if pm is 1 (red), a piece that is -1 (black) will be < 0 and also fail.

   for (var pm = -1; pm == -1 || pm == 1; pm += 2) {
      // This for loop will run once for -1, once for 1, computing both player's scores.
      // First test: Vertical pattern test
      // It is not necessary to check last three rows unless red occupies fourth from last spot.
	numPM = 0;

      for (var vscan = 0; vscan < numRows - 3; vscan++) {
         for (var vcol = 0; vcol < numCols; vcol++) {
            if (boardH1[vscan][vcol] * pm >= 0) {
               if (boardH1[vscan+1][vcol] * pm >= 0 && boardH1[vscan+2][vcol] * pm >= 0 && boardH1[vscan+3][vcol] * pm >= 0) {

                  // Pattern found - vertical. Check # of empty spaces, must be exactly one
			sumNotOp = 0;	// Reset sum of pieces not opponent
			sumNotOp = boardH1[vscan][vcol] + boardH1[vscan+1][vcol] + boardH1[vscan+2][vcol] + boardH1[vscan+3][vcol];
			if (Math.abs(sumNotOp) == 3) {
				// sumNotOp is positive int for number of computer's pieces (not gaps) in the line
				// of four which may contain only computer's pieces or gaps. 1 gap = 3 pieces.
                  	numPM++;	// Add one match for current value of PM
			}
               }
		}
	   }
      }

      // Second test: Horizontal pattern test
      // It is not necessary to check last three cols unless red occupies fourth from last spot.
      for (var hscan = 0; hscan < numCols - 3; hscan++) {
         for (var hrow = 0; hrow < numRows; hrow++) {
            if (boardH1[hrow][hscan] * pm >= 0) {
               if (boardH1[hrow][hscan+1] * pm >= 0 && boardH1[hrow][hscan+2] * pm >= 0 && boardH1[hrow][hscan+3] * pm >= 0) {
                  // Pattern found - horizontal. Check # of empty spaces, must be exactly one
			sumNotOp = 0;	// Reset sum of pieces not opponent
			sumNotOp = boardH1[hrow][hscan] + boardH1[hrow][hscan+1] + boardH1[hrow][hscan+2] + boardH1[hrow][hscan+3];
			if (Math.abs(sumNotOp) == 3) {
				// sumNotOp is positive int for number of computer's pieces (not gaps) in the line
				// of four which may contain only computer's pieces or gaps. 1 gap = 3 pieces.
                  	numPM++;	// Add one match for current value of PM
			}
		   }
		}
	   }
      }

      // Third test: Forward Slash "/" Diagonal pattern test
      // It is not necessary to check last three cols unless red occupies fourth from last spot.
      // Start in row 3, because pattern cannot begin above that row.
      for (var fscan = 0; fscan < numCols - 3; fscan++) {
         for (var frow = 3; frow < numRows; frow++) {
            if (boardH1[frow][fscan] * pm >= 0) {
               if (boardH1[frow-1][fscan+1] * pm >= 0 && boardH1[frow-2][fscan+2] * pm >= 0 && boardH1[frow-3][fscan+3] * pm >= 0) {
                  // Pattern found - forward slash. Check # of empty spaces, must be exactly one
			sumNotOp = 0;	// Reset sum of pieces not opponent
			sumNotOp = boardH1[frow][fscan] + boardH1[frow-1][fscan+1] + boardH1[frow-2][fscan+2] + boardH1[frow-3][fscan+3];
			if (Math.abs(sumNotOp) == 3) {
				// sumNotOp is positive int for number of computer's pieces (not gaps) in the line
				// of four which may contain only computer's pieces or gaps. 1 gap = 3 pieces.
                  	numPM++;	// Add one match for current value of PM
			}
               }
		}
	   }
      }

      // Fourth test: Back Slash "\" Diagonal pattern test
      // It is not necessary to check last three cols unless red occupies fourth from last spot.
      // Stop 3 rows short, because pattern cannot end below that row.
      for (var bscan = 0; bscan < numCols - 3; bscan++) {
         for (var brow = 0; brow < numRows - 3; brow++) {
            if (boardH1[brow][bscan] * pm >= 0) {
               if (boardH1[brow+1][bscan+1] * pm >= 0 && boardH1[brow+2][bscan+2] * pm >= 0 && boardH1[brow+3][bscan+3] * pm >= 0) {
                  // Pattern found - back slash. Check # of empty spaces, must be exactly one
			sumNotOp = 0;	// Reset sum of pieces not opponent
			sumNotOp = boardH1[brow][bscan] + boardH1[brow+1][bscan+1] + boardH1[brow+2][bscan+2] + boardH1[brow+3][bscan+3];
			if (Math.abs(sumNotOp) == 3) {
				// sumNotOp is positive int for number of computer's pieces (not gaps) in the line
				// of four which may contain only computer's pieces or gaps. 1 gap = 3 pieces.
                  	numPM++;	// Add one match for current value of PM
			}
               }
		}
	   }
      }

	// Finished testing, store results
      if (pm == -1) {
	   // Black
         numBlk = numPM;
	}
	else {
	   // Red
	   numRed = numPM;
	}
   }

   var blkMult = 1, redMult = -1;	// Multipliers for pieces, assuming black computer player
   if (turn == 1) {
	// Red computer player
	blkMult = -1;
	redMult = 1;
   }

   var forWhammy = cpn1forWhammy;

   if (turn == cp1) {
      forWhammy = cp1forWhammy;
   }

   retval = ((blkMult * Math.pow(forWhammy, numBlk)) + (redMult * Math.pow(forWhammy, numRed)));
   if (retval < -10000) {retval = -10000;}		// This solves ridiculously high exponential output
   if (retval > 10000) {retval = 10000;}

   return retval;
}

function isBlocked(boardH2) {
   // Tests whether all enemy patterns are blocked.   xooox  xooo]
   // Blocking enemy patterns is crucial to success. By adding a heuristic scoring blocked
   // better than unblocked, we can cure a blind spot at the maxDepth of the game tree.
   // Of course being blocked can prevent one's own win, so this function will also score blocked
   // computer patterns negatively. These are separate variables that can be tinkered with.
   // This will produce positive points for blocking and negative points for being blocked.
   // Points are a multiple of constant C times number of blocks, +/- depending on player.

   var numRed = 0;		// Number of Red blocks.
   var numBlk = 0;		// Same for black.
   var numPM = 0;			// Number of matches for current value of pm
   var retval = 0;		// Return value.
   var sumNotOp = 0;		// Sum of pieces in line of four that do not belong to opponent

   // This test, like Whammy, uses a modified form of the checkWin search. In red's case, instead of
   // looking for four red pieces, we look for one red piece and three holes.

   // The test operations will compare (cell value * pm) != 0 for match of holes or pm pieces.
   // This way, if pm is -1 (black), a piece that is 1 (red) will be < 0 and fail.
   // Alternatively, if pm is 1 (red), a piece that is -1 (black) will be < 0 and also fail.

   for (var pm = -1; pm == -1 || pm == 1; pm += 2) {
      // This for loop will run once for -1, once for 1, computing both player's scores.
      // First test: Vertical pattern test
      // It is not necessary to check last three rows unless red occupies fourth from last spot.
	numPM = 0;

      for (var vscan = 0; vscan < numRows - 3; vscan++) {
         for (var vcol = 0; vcol < numCols; vcol++) {
            if (boardH2[vscan][vcol] != 0) {
               if (boardH2[vscan+1][vcol] != 0 && boardH2[vscan+2][vcol] != 0 && boardH2[vscan+3][vcol] != 0) {

                  // Four pieces (any color) in a row.
			// If pm is blocking, value will be 2.
			// e.g. pm = -1, -1 + 1 + 1 + 1 = 2, pm = 1, 1 + -1 + -1 + -1 = -2
			// Test for match: sum * pm = -2.

			sumNotOp = 0;	// Reset sum of pieces not opponent
			sumNotOp = boardH2[vscan][vcol] + boardH2[vscan+1][vcol] + boardH2[vscan+2][vcol] + boardH2[vscan+3][vcol];
			if (sumNotOp * pm == -2) {
				// sumNotOp is signed int for number of pm's pieces (not opponent's) in the line
				// of four which may contain only pieces. 3 opponents, 1 computer, is a block for computer.
                  	numPM++;	// Add one match for current value of PM
			}
               }
		}
	   }
      }

      // Second test: Horizontal pattern test
      // It is not necessary to check last three cols unless red occupies fourth from last spot.
      for (var hscan = 0; hscan < numCols - 3; hscan++) {
         for (var hrow = 0; hrow < numRows; hrow++) {
            if (boardH2[hrow][hscan] != 0) {
               if (boardH2[hrow][hscan+1] != 0 && boardH2[hrow][hscan+2] != 0 && boardH2[hrow][hscan+3] != 0) {

                  // Four pieces (any color) in a row.
			// If pm is blocking, value will be 2.
			// e.g. pm = -1, -1 + 1 + 1 + 1 = 2, pm = 1, 1 + -1 + -1 + -1 = -2
			// Test for match: sum * pm = -2.

			sumNotOp = 0;	// Reset sum of pieces not opponent
			sumNotOp = boardH2[hrow][hscan] + boardH2[hrow][hscan+1] + boardH2[hrow][hscan+2] + boardH2[hrow][hscan+3];
			if (sumNotOp * pm == -2) {
				// sumNotOp is signed int for number of pm's pieces (not opponent's) in the line
				// of four which may contain only pieces. 3 opponents, 1 computer, is a block for computer.
                  	numPM++;	// Add one match for current value of PM
			}
		   }
		}
	   }
      }

      // Third test: Forward Slash "/" Diagonal pattern test
      // It is not necessary to check last three cols unless red occupies fourth from last spot.
      // Start in row 3, because pattern cannot begin above that row.
      for (var fscan = 0; fscan < numCols - 3; fscan++) {
         for (var frow = 3; frow < numRows; frow++) {
            if (boardH2[frow][fscan] != 0) {
               if (boardH2[frow-1][fscan+1] != 0 && boardH2[frow-2][fscan+2] != 0 && boardH2[frow-3][fscan+3] != 0) {

                  // Four pieces (any color) in a row.
			// If pm is blocking, value will be 2.
			// e.g. pm = -1, -1 + 1 + 1 + 1 = 2, pm = 1, 1 + -1 + -1 + -1 = -2
			// Test for match: sum * pm = -2.

			sumNotOp = 0;	// Reset sum of pieces not opponent
			sumNotOp = boardH2[frow][fscan] + boardH2[frow-1][fscan+1] + boardH2[frow-2][fscan+2] + boardH2[frow-3][fscan+3];
			if (sumNotOp * pm == -2) {
				// sumNotOp is signed int for number of pm's pieces (not opponent's) in the line
				// of four which may contain only pieces. 3 opponents, 1 computer, is a block for computer.
                  	numPM++;	// Add one match for current value of PM
			}
               }
		}
	   }
      }

      // Fourth test: Back Slash "\" Diagonal pattern test
      // It is not necessary to check last three cols unless red occupies fourth from last spot.
      // Stop 3 rows short, because pattern cannot end below that row.
      for (var bscan = 0; bscan < numCols - 3; bscan++) {
         for (var brow = 0; brow < numRows - 3; brow++) {
            if (boardH2[brow][bscan] != 0) {
               if (boardH2[brow+1][bscan+1] != 0 && boardH2[brow+2][bscan+2] != 0 && boardH2[brow+3][bscan+3] != 0) {

                  // Four pieces (any color) in a row.
			// If pm is blocking, value will be 2.
			// e.g. pm = -1, -1 + 1 + 1 + 1 = 2, pm = 1, 1 + -1 + -1 + -1 = -2
			// Test for match: sum * pm = -2.

			sumNotOp = 0;	// Reset sum of pieces not opponent
			sumNotOp = boardH2[brow][bscan] + boardH2[brow+1][bscan+1] + boardH2[brow+2][bscan+2] + boardH2[brow+3][bscan+3];
			if (sumNotOp * pm == -2) {
				// sumNotOp is signed int for number of pm's pieces (not opponent's) in the line
				// of four which may contain only pieces. 3 opponents, 1 computer, is a block for computer.
                  	numPM++;	// Add one match for current value of PM
			}
               }
		}
	   }
      }

	// Finished testing, store results
      if (pm == -1) {
	   // Black
         numBlk = numPM;
	}
	else {
	   // Red
	   numRed = numPM;
	}
   }

   var forBlockBlk = cpn1forBlocking;
   var forBlockRed = cpn1forBlocked;

   if (turn == cp1) {
	// Note reversed colors for blocking/blocked
      forBlockBlk = cp1forBlocked;
      forBlockRed = cp1forBlocking;
   }

   retval = (forBlockBlk * numBlk) + (forBlockRed * numRed);
   return retval;
}

function roadsToOpportunity(boardH3) {
   // This heuristic will reward a piece for choosing a
   // spot with many options for expansion into four-in-a-row.               +  +  +
   // In a perfect Connect Four game the first player occupies                + + +
   // the center because it has the most "roads." This heuristic               +++    X can expand in
   // will help the AI pick a move with the most roads.                      +++x+++  direction of +'s
   // This heuristic will be most useful during the beginning of the game.
   // Negative points for all opponent roads to opportunity.
   // Scored as a multiple of constant C.

   var numRed = 0;		// Number of Red roads.
   var numBlk = 0;		// Same for black.
   var numPM = 0;			// Number of matches for current value of pm
   var retval = 0;		// Return value.
   var sumNotOp = 0;		// Sum of pieces in line of four that do not belong to opponent

   // This test, like Whammy, uses a modified form of the checkWin search. In red's case, instead of
   // looking for four red pieces, we look for one red piece and three holes.

   // The test operations will compare (cell value * pm) >= 0 for match of holes or pm pieces.
   // This way, if pm is -1 (black), a piece that is 1 (red) will be < 0 and fail.
   // Alternatively, if pm is 1 (red), a piece that is -1 (black) will be < 0 and also fail.

   for (var pm = -1; pm == -1 || pm == 1; pm += 2) {
      // This for loop will run once for -1, once for 1, computing both player's scores.
      // First test: Vertical pattern test
      // It is not necessary to check last three rows unless red occupies fourth from last spot.
	numPM = 0;

      for (var vscan = 0; vscan < numRows - 3; vscan++) {
         for (var vcol = 0; vcol < numCols; vcol++) {
            if (boardH3[vscan][vcol] * pm >= 0) {
               if (boardH3[vscan+1][vcol] * pm >= 0 && boardH3[vscan+2][vcol] * pm >= 0 && boardH3[vscan+3][vcol] * pm >= 0) {

                  // Pattern found - vertical. Check # of empty spaces, must be exactly one
			sumNotOp = 0;	// Reset sum of pieces not opponent
			sumNotOp = boardH3[vscan][vcol] + boardH3[vscan+1][vcol] + boardH3[vscan+2][vcol] + boardH3[vscan+3][vcol];
			if (Math.abs(sumNotOp) == 1) {
				// sumNotOp is positive int for number of computer's pieces (not gaps) in the line
				// of four which may contain only computer's pieces or gaps. 3 gaps = 1 piece.
                  	numPM++;	// Add one match for current value of PM
			}
               }
		}
	   }
      }

      // Second test: Horizontal pattern test
      // It is not necessary to check last three cols unless red occupies fourth from last spot.
      for (var hscan = 0; hscan < numCols - 3; hscan++) {
         for (var hrow = 0; hrow < numRows; hrow++) {
            if (boardH3[hrow][hscan] * pm >= 0) {
               if (boardH3[hrow][hscan+1] * pm >= 0 && boardH3[hrow][hscan+2] * pm >= 0 && boardH3[hrow][hscan+3] * pm >= 0) {
                  // Pattern found - horizontal. Check # of empty spaces, must be exactly one
			sumNotOp = 0;	// Reset sum of pieces not opponent
			sumNotOp = boardH3[hrow][hscan] + boardH3[hrow][hscan+1] + boardH3[hrow][hscan+2] + boardH3[hrow][hscan+3];
			if (Math.abs(sumNotOp) == 1) {
				// sumNotOp is positive int for number of computer's pieces (not gaps) in the line
				// of four which may contain only computer's pieces or gaps. 3 gaps = 1 piece.
                  	numPM++;	// Add one match for current value of PM
			}
		   }
		}
	   }
      }

      // Third test: Forward Slash "/" Diagonal pattern test
      // It is not necessary to check last three cols unless red occupies fourth from last spot.
      // Start in row 3, because pattern cannot begin above that row.
      for (var fscan = 0; fscan < numCols - 3; fscan++) {
         for (var frow = 3; frow < numRows; frow++) {
            if (boardH3[frow][fscan] * pm >= 0) {
               if (boardH3[frow-1][fscan+1] * pm >= 0 && boardH3[frow-2][fscan+2] * pm >= 0 && boardH3[frow-3][fscan+3] * pm >= 0) {
                  // Pattern found - forward slash. Check # of empty spaces, must be exactly one
			sumNotOp = 0;	// Reset sum of pieces not opponent
			sumNotOp = boardH3[frow][fscan] + boardH3[frow-1][fscan+1] + boardH3[frow-2][fscan+2] + boardH3[frow-3][fscan+3];
			if (Math.abs(sumNotOp) == 1) {
				// sumNotOp is positive int for number of computer's pieces (not gaps) in the line
				// of four which may contain only computer's pieces or gaps. 3 gaps = 1 piece.
                  	numPM++;	// Add one match for current value of PM
			}
               }
		}
	   }
      }

      // Fourth test: Back Slash "\" Diagonal pattern test
      // It is not necessary to check last three cols unless red occupies fourth from last spot.
      // Stop 3 rows short, because pattern cannot end below that row.
      for (var bscan = 0; bscan < numCols - 3; bscan++) {
         for (var brow = 0; brow < numRows - 3; brow++) {
            if (boardH3[brow][bscan] * pm >= 0) {
               if (boardH3[brow+1][bscan+1] * pm >= 0 && boardH3[brow+2][bscan+2] * pm >= 0 && boardH3[brow+3][bscan+3] * pm >= 0) {
                  // Pattern found - back slash. Check # of empty spaces, must be exactly one
			sumNotOp = 0;	// Reset sum of pieces not opponent
			sumNotOp = boardH3[brow][bscan] + boardH3[brow+1][bscan+1] + boardH3[brow+2][bscan+2] + boardH3[brow+3][bscan+3];
			if (Math.abs(sumNotOp) == 1) {
				// sumNotOp is positive int for number of computer's pieces (not gaps) in the line
				// of four which may contain only computer's pieces or gaps. 3 gaps = 1 piece.
                  	numPM++;	// Add one match for current value of PM
			}
               }
		}
	   }
      }

	// Finished testing, store results
      if (pm == -1) {
	   // Black
         numBlk = numPM;
	}
	else {
	   // Red
	   numRed = numPM;
	}
   }

   var blkMult = 1, redMult = -1;	// Multipliers for pieces, assuming black computer player
   if (turn == cp1) {
	// Red computer player
	blkMult = -1;
	redMult = 1;
   }

   var forRoads = cpn1forRoads;

   if (turn == cp1) {
      forRoads = cp1forRoads;
   }

   retval = (blkMult * forRoads * numBlk) + (redMult * forRoads * numRed);
   return retval;
}

function hasOddEvenWins(boardH4) {
   // This heuristic rewards patterns that win on both an even and an odd row. This should be contrasted with
   // the whammy heuristic which does not consider even/odd, which is an important rule of thumb in Connect Four.
   // The two heuristics are not mutually exclusive, both should be combined as both togther are good strategies
   // to have. A coveted board position is one that offers multiple wins on the same row and among different rows,
   // without affording the same position to the opponent. This was a particular weakness of the computer before this
   // heuristic. It is scored as # of odd/even wins (0 or 1) times constant C times +/-1 depending on player.

   // For example, these positions guarantee x a win at either ':

   //                      '
   //       '             x'
   //    xxx'            xox
   //    oxox           xoox
   // o  xooo          oxxox

   var numRed = 0;		// Number of Red OE pairs. (Note: As currently implemented, only one is counted.)
   var numBlk = 0;		// Same for black.
   var numPM = 0;			// Number of matches for current value of pm
   var numPMO = 0;		// Number of odd wins for current pm
   var numPME = 0;		// Number of even wins for current pm
   var numblo = 0;		// Number of pm pieces below (used by each iteration of checkblo)
   var checkblo;
   var numside = 0;		// Number of pm pieces to side (used by each iteration of checkside)
   var checkside;
   var numfs = 0;			// Number of pm pieces on fwd slash (used by each iteration of checkfs)
   var checkfs;
   var checkfscol;
   var numbs = 0;			// Number of pm pieces on back slash (used by each iteration of checkbs)
   var checkbs;
   var checkbscol;
   var retval = 0;		// Return value.

   // This test is different. One possibility for implementing was just placing the piece and checking win,
   // but that would be costly (n^2) to process. Instead a constant-time algorithm is used to check the immediate
   // area for a pattern.

   for (var pm = -1; pm == -1 || pm == 1; pm += 2) {
      // This for loop will run once for -1, once for 1, computing both player's scores.

      for (var b = 0; b < numCols; b++) {
         numPMO = 0;
	   numPME = 0;
	   for (var a = 0; a < numRows; a++) {

            // Scan every hole (ignore occupied spaces)

            if (!boardH4[a][b]) {
               // Space is 0, so it is a hole

		   // First test: Vertical pattern test
               // If the three spots below are occupied by pm, this spot has a win.
               // (Having a win means that at odd, you have an odd win, at even, an even win. Both scores a point
               // with this heuristic.)
  		   numblo = 0;		// Reset number of pm pieces below
		   checkblo = a + 1;    // Check space below current
		   while (checkblo <= a + 3 && checkblo < numRows && boardH4[checkblo][b] == pm) {
			// This space is occupied by pm
			numblo++;
		      checkblo++;
		   }
               if (numblo == 3) {
			// We have a win at this spot
			// Determine odd/even using isMin() which is func. equiv. to isOdd()
			if (isMin(a)) {
			   // Odd win
			   numPMO++;
			}
			else {
			   numPME++;
			}
		   }

		   // Second test: Side pattern test
               // If pm's pieces to left and pieces to right add up to 3, this spot has a win.
  		   numside = 0;		// Reset number of pm pieces on side
		   checkside = b - 1;	// Check space left of current
		   while (checkside >= b - 3 && checkside >= 0 && boardH4[a][checkside] == pm) {
			// This space is occupied by pm
			numside++;
			checkside--;
		   }
		   checkside = b + 1;	// Check space right of current
		   while (checkside <= b + 3 && checkside < numCols && boardH4[a][checkside] == pm) {
			// This space is occupied by pm
			numside++;
			checkside++;
		   }
               if (numside >= 3) {
			// We have a win at this spot
			// Determine odd/even using isMin() which is func. equiv. to isOdd()
			if (isMin(a)) {
			   // Odd win
			   numPMO++;
			}
			else {
			   numPME++;
			}
		   }

		   // Third test: Forward Slash pattern test
               // If pm's pieces in both directions add up to 3, this spot has a win.
  		   numfs = 0;		// Reset number of pm pieces in forward slash
		   checkfs = a + 1;	// Check space left-below of current
		   checkfscol = b - 1;	// Check space left-below of current
		   while (checkfs <= a + 3 && checkfs < numRows && checkfscol >= b - 3 && checkfscol >= 0 && boardH4[checkfs][checkfscol] == pm) {
			// This space is occupied by pm
			numfs++;
			checkfs++;
			checkfscol--;
		   }
		   checkfs = a - 1;	// Check space right-above current
		   checkfscol = b + 1;	// Check space right-above current
		   while (checkfs >= a - 3 && checkfs >= 0 && checkfscol <= b + 3 && checkfscol < numCols && boardH4[checkfs][checkfscol] == pm) {
			// This space is occupied by pm
			numfs++;
			checkfs--;
			checkfscol++;
		   }
               if (numfs >= 3) {
			// We have a win at this spot
			// Determine odd/even using isMin() which is func. equiv. to isOdd()
			if (isMin(a)) {
			   // Odd win
			   numPMO++;
			}
			else {
			   numPME++;
			}
		   }

		   // Fourth test: Back Slash pattern test
               // If pm's pieces in both directions add up to 3, this spot has a win.
  		   numbs = 0;		// Reset number of pm pieces in forward slash
		   checkbs = a + 1;	// Check space right-below of current
		   checkbscol = b + 1;	// Check space right-below of current
		   while (checkbs <= a + 3 && checkbs < numRows && checkbscol <= b + 3 && checkbscol < numCols && boardH4[checkbs][checkbscol] == pm) {
			// This space is occupied by pm
			numbs++;
			checkbs++;
			checkbscol++;
		   }
		   checkbs = a - 1;	// Check space left-above current
		   checkbscol = b - 1;	// Check space left-above current
		   while (checkbs >= a - 3 && checkbs >= 0 && checkbscol >= b - 3 && checkbscol >= 0 && boardH4[checkbs][checkbscol] == pm) {
                  // This space is occupied by pm
			numbs++;
			checkbs--;
			checkbscol--;
		   }
               if (numbs >= 3) {
			// We have a win at this spot
			// Determine odd/even using isMin() which is func. equiv. to isOdd()
			if (isMin(a)) {
			   // Odd win
			   numPMO++;
			}
			else {
			   numPME++;
			}
		   }

		}
         }
         // Just went through all rows for this column
         // Now check numPMO and numPME, if both are not zero, then this column has both odd and even wins
         if (numPMO && numPME) {
		numPM++;
		break;	// Break out once one found, only one counts for this heuristic
	   }
      }

	// Finished testing, store results
      if (pm == -1) {
	   // Black
         numBlk = numPM;
	}
	else {
	   // Red
	   numRed = numPM;
	}
   }

   var forOE = cpn1forOE;

   if (turn == cp1) {
      forOE = cp1forOE;
   }

   var blkMult = 1, redMult = -1;	// Multipliers for pieces, assuming black computer player
   if (turn == 1) {
	// Red computer player
	blkMult = -1;
	redMult = 1;
   }

   retval = (blkMult * forOE * numBlk) + (redMult * forOE * numRed);
   return retval;
}

