Code viewer for World: Cloned Checkers

// Cloned by Teacher World samples on 10 Dec 2017 from World "Checkers" by Adrien HARNAY 
// Please leave this clone trail here.
 



// =============================================================================================
// Checkers World
// Adrien HARNAY
// Simulation of a Checkers Board where you can play with a "Mind" against the "World"
// =============================================================================================


// =============================================================================================
// Scoring:
// You either score 100 or 0 depending on whether you win the game or not
// =============================================================================================


// World must define these:
 
const	 	CLOCKTICK 	= 1000;					// speed of run - move things every n milliseconds
const		MAXSTEPS 	= 1000000;				// length of a run before final score

const  SCREENSHOT_STEP = 1;


//---- global constants: -------------------------------------------------------

const gridsize = 8;						// number of squares along side of world	   
	
const SKYCOLOR 	= 0xddffdd;				// a number, not a string 
const BLANKCOLOR 	= SKYCOLOR ;			// make objects this color until texture arrives (from asynchronous file read)

const show3d = true;						// Switch between 3d and 2d view (both using Three.js) 

const squaresize = 30;					// size of square in pixels
const MAXPOS = gridsize * squaresize;		// length of one side in pixels 
const startRadiusConst	 	= MAXPOS * 0.8 ;		// distance from centre to start the camera at
const skyboxConst			= MAXPOS * 3 ;		// where to put skybox 
const maxRadiusConst 		= MAXPOS * 10  ;		// maximum distance from camera we will render things  

// in initial view, (smaller-larger) on i axis is aligned with (left-right)
// in initial view, (smaller-larger) on j axis is aligned with (away from you - towards you)

// contents of a grid square

const WHITE_CELL 	= 0;
const BLACK_CELL 	= 1;
const WHITE_PIECE 	= 2;
const BLACK_PIECE 	= 3;

const WORLD_COLOR = BLACK_PIECE;
const MIND_COLOR = WHITE_PIECE;

// textures just in case

var BLACK_CELL_OBJ = new Array(gridsize * gridsize / 2);
var blackCellObjCount = 0;

//---- start of World class -------------------------------------------------------
 
function World() { 


// most of World can be private 
// regular "var" syntax means private variables:

var BOXHEIGHT;		// 3d or 2d box height 

var BOARD 	= new Array(gridsize * gridsize);			// can query BOARD about whether squares are occupied, will in fact be initialised as a 2D array   
var GRAPH_BOARD 	= new Array (gridsize * gridsize);

var turn;
var winner;
var noMoreMoves;

var self = this;						// needed for private fn to call public fn - see below  

// regular "function" syntax means private functions:

function initBoard()
{
    for (var i = 0; i < gridsize * 1; i += 2) {
        BOARD[i] = WHITE_CELL;
        BOARD[i + 1] = BLACK_PIECE;
        BOARD[i + gridsize * 2] = WHITE_CELL;
        BOARD[i + gridsize * 2 + 1] = BLACK_PIECE;
    }
    for (i = gridsize * 1; i < gridsize * 2; i += 2) {
        BOARD[i] = BLACK_PIECE;
        BOARD[i + 1] = WHITE_CELL;
    }
    for (i = gridsize * 3; i < gridsize * 4; i += 2) {
        BOARD[i] = BLACK_CELL;
        BOARD[i + 1] = WHITE_CELL;
    }
    for (i = gridsize * 4; i < gridsize * 5; i += 2) {
        BOARD[i] = WHITE_CELL;
        BOARD[i + 1] = BLACK_CELL;
    }
    for (i = gridsize * 5; i < gridsize * 6; i += 2) {
        BOARD[i] = WHITE_PIECE;
        BOARD[i + 1] = WHITE_CELL;
        BOARD[i + gridsize * 2] = WHITE_PIECE;
        BOARD[i + gridsize * 2 + 1] = WHITE_CELL;
    }
    for (i = gridsize * 6; i < gridsize * 7; i += 2) {
        BOARD[i] = WHITE_CELL;
        BOARD[i + 1] = WHITE_PIECE;
    }
}

//--- skybox ----------------------------------------------------------------------------------------------


function initSkybox() 
{

// x,y,z positive and negative faces have to be in certain order in the array 
 
// mountain skybox, credit:
// http://stemkoski.github.io/Three.js/Skybox.html

  var materialArray = [
 	( new THREE.MeshBasicMaterial ( { map: THREE.ImageUtils.loadTexture( "/uploads/starter/dawnmountain-xpos.png" ), side: THREE.BackSide } ) ),
 	( new THREE.MeshBasicMaterial ( { map: THREE.ImageUtils.loadTexture( "/uploads/starter/dawnmountain-xneg.png" ), side: THREE.BackSide } ) ),
 	( new THREE.MeshBasicMaterial ( { map: THREE.ImageUtils.loadTexture( "/uploads/starter/dawnmountain-ypos.png" ), side: THREE.BackSide } ) ),
 	( new THREE.MeshBasicMaterial ( { map: THREE.ImageUtils.loadTexture( "/uploads/starter/dawnmountain-yneg.png" ), side: THREE.BackSide } ) ),
 	( new THREE.MeshBasicMaterial ( { map: THREE.ImageUtils.loadTexture( "/uploads/starter/dawnmountain-zpos.png" ), side: THREE.BackSide } ) ),
 	( new THREE.MeshBasicMaterial ( { map: THREE.ImageUtils.loadTexture( "/uploads/starter/dawnmountain-zneg.png" ), side: THREE.BackSide } ) )
 	];

  var skyGeometry = new THREE.CubeGeometry ( skyboxConst, skyboxConst, skyboxConst );	
  var skyMaterial = new THREE.MeshFaceMaterial ( materialArray );
  var theskybox = new THREE.Mesh ( skyGeometry, skyMaterial );
  threeworld.scene.add( theskybox );						// We are inside a giant cube
}

// Graphics functions

function paintBoard ( whiteCellTexture, blackCellTexture, whitePieceTexture, blackPieceTexture )		 
{
  if (!whiteCellTexture || !blackCellTexture || !whitePieceTexture || !blackPieceTexture ) {
    return;
  }
  
  for ( var i = 0; i < BLACK_CELL_OBJ.length; i++ )
 { 
  BLACK_CELL_OBJ[i].material = blackCellTexture;
 }
  
 for ( i = 0; i < BOARD.length; i++ )
 { 
   if (GRAPH_BOARD[i]) {
     switch (BOARD[i]) {
       case WHITE_CELL:
        GRAPH_BOARD[i].material = whiteCellTexture;
       break;
       case BLACK_CELL:
        GRAPH_BOARD[i].material = blackCellTexture;
       break;
       case WHITE_PIECE:
        GRAPH_BOARD[i].material = whitePieceTexture;
       break;
       case BLACK_PIECE:
        GRAPH_BOARD[i].material = blackPieceTexture;
       break;
       default:
     }
   } 
 }
}

function drawCellOrPiece(obj, x, z) {
  obj.position.x = translate(x * squaresize);   	
  obj.position.z = translate(z  * squaresize);   	
  obj.position.y = 0;
 
 if ( true  )
  {
    threeworld.scene.add(obj);
  }
}

function loadTextures() {
  var whiteCellTexture;
  var blackCellTexture;
  var whitePieceTexture;
  var blackPieceTexture;
  
  var loader1 = new THREE.TextureLoader();
  loader1.load ( '/uploads/starter/latin.jpg',		function ( thetexture ) {			 
    thetexture.minFilter = THREE.LinearFilter;
    whiteCellTexture = new THREE.MeshBasicMaterial( { map: thetexture } );
    paintBoard(whiteCellTexture, blackCellTexture, whitePieceTexture, blackPieceTexture);
  } ); 
  var loader2 = new THREE.TextureLoader();
  loader2.load ( '/uploads/starter/door.jpg',		function ( thetexture ) {			 
    thetexture.minFilter = THREE.LinearFilter;
    blackCellTexture = new THREE.MeshBasicMaterial( { map: thetexture } );
    paintBoard(whiteCellTexture, blackCellTexture, whitePieceTexture, blackPieceTexture);
  } ); 
  var loader3 = new THREE.TextureLoader();
  loader3.load ( '/uploads/starter/pacman.jpg',		function ( thetexture ) {			 
    thetexture.minFilter = THREE.LinearFilter;
    whitePieceTexture = new THREE.MeshBasicMaterial( { map: thetexture } );
    paintBoard(whiteCellTexture, blackCellTexture, whitePieceTexture, blackPieceTexture);
  } ); 
  var loader4 = new THREE.TextureLoader();
  loader4.load ( '/uploads/starter/ghost.3.png',		function ( thetexture ) {			 
    thetexture.minFilter = THREE.LinearFilter;
    blackPieceTexture = new THREE.MeshBasicMaterial( { map: thetexture } );
    paintBoard(whiteCellTexture, blackCellTexture, whitePieceTexture, blackPieceTexture);
  } );
}

function initGraphBoard()		  	
{
  var shape;
  var thecube;
  
 for (var i = 0; i < gridsize * 8 ; i++) {
  shape    = new THREE.BoxGeometry( squaresize, BOXHEIGHT, squaresize );			 
  thecube  = new THREE.Mesh( shape );
  
  thecube.material.color.setHex( BLANKCOLOR  );			
  GRAPH_BOARD[i] = thecube;		// save it for later  

  drawCellOrPiece(thecube, getXFromI(i), getZFromI(i));
 }
  
  for (var i = 0; i < BLACK_CELL_OBJ.length ; i++) {
    shape = new THREE.BoxGeometry( squaresize, BOXHEIGHT, squaresize );
    thecube  = new THREE.Mesh( shape );
    thecube.material.color.setHex( BLANKCOLOR  );
    BLACK_CELL_OBJ[i] = thecube;
  }
}

function updateStatusBefore()
{
 var status = " Turn: <b> " + (turn === WHITE_PIECE ? 'PacMan (Mind)' : 'Pirates (World)') + " </b>"; 

 $("#user_span3").html( status );
}

// win & turn functions

function changeTurn() {
  turn = turn === WHITE_PIECE ? BLACK_PIECE : WHITE_PIECE;
}

function checkWin() {
  var count = countPieces(BOARD);
  
  if (count.black === 0) {
    winner = 'PacMan (Mind)';
    return true;
  }
  if (count.white === 0) {
    winner = 'Pirates (World)';
    return true;
  }
  
  if (noMoreMoves) {
    if (count.black >= count.white) {
      winner = 'Pirates (World)';
    } else {
      winner = 'PacMan (Mind)';
    }
    
    return true;
  }
  
  return false;
}

// utils random

function randomFloatAtoB ( A, B )			 
{
 return ( A + ( Math.random() * (B-A) ) );
}

function randomIntAtoB ( A, B )			 
{
 return  ( Math.round ( randomFloatAtoB ( A, B ) ) );
}

// utils coordinate functions

function translate (pos) 
{
 return (pos - (MAXPOS / 2));
}

function isCorrectPos(pos) {
  return pos >= 0 && pos <= 7;
}

function getXFromI(i) {
  return i % gridsize;
}

function getZFromI(i) {
  return Math.floor(i / gridsize);
}

function getIFromXZ(x, z) {
  return z * gridsize + x;
}

// world move functions

function replaceEatenCell(playerColor, i, board, graphBoard) {
  var pieceColor = (playerColor === WHITE_PIECE ? BLACK_PIECE : WHITE_PIECE);
  
  board[i] = (board[i] === BLACK_CELL ? pieceColor : BLACK_CELL);
  
  if (graphBoard) {
    drawCellOrPiece(graphBoard[i], 1000000, 1000000);
    graphBoard[i] = BLACK_CELL_OBJ[blackCellObjCount];
    blackCellObjCount++;
    drawCellOrPiece(graphBoard[i], getXFromI(i), getZFromI(i));
  }
}

function getEatenCellI(startI, endI) {
  var startX = getXFromI(startI);
  var startZ = getZFromI(startI);
  var endX = getXFromI(endI);
  var endZ = getZFromI(endI);
  
  var zDiff = endZ - startZ;
  var xDiff = endX - startX;
  
  if (!(zDiff === 2 || zDiff == -2)) {
    return -1;
  }
  
  return getIFromXZ(endX - xDiff / 2, endZ - zDiff / 2);
}

function canMove(playerColor, startI, endI, board) {
  var piece = board[startI];
  var destination = board[endI];
  var enemyColor = playerColor === WHITE_PIECE ? BLACK_PIECE : WHITE_PIECE;
  
  var startX = getXFromI(startI);
  var startZ = getZFromI(startI);
  var endX = getXFromI(endI);
  var endZ = getZFromI(endI);
  
  if (destination !== BLACK_CELL
  || !isCorrectPos(endX) || !isCorrectPos(endZ)
  || (piece === BLACK_PIECE && (playerColor !== BLACK_PIECE || startI >= endI))
  || (piece === WHITE_PIECE && (playerColor !== WHITE_PIECE || startI <= endI))) {
     return false;
   }
   
   var zDiff1 = piece === BLACK_PIECE ? 1 : -1;
   var zDiff2 = piece === BLACK_PIECE ? 2 : -2;
   
   if (!(endZ === startZ + zDiff1 && (endX === startX + 1 || endX === startX - 1))
      && !(endZ === startZ + zDiff2 && (endX === startX + 2 || endX === startX - 2))) {
    return false;
   }
   
   if (endZ === startZ + zDiff2
   && ((endX === startX + 2 && board[getIFromXZ(startX + 1, startZ + zDiff1)] !== enemyColor)
     || (endX === startX - 2 && board[getIFromXZ(startX - 1, startZ + zDiff1)] !== enemyColor))) {
       return false;
     }
    
    return true;
}

function move(playerColor, startI, endI, board, graphBoard, force) {
  if (!force && !canMove(playerColor, startI, endI, board)) {
    return false;
  }
  
  var eatenCellI = getEatenCellI(startI, endI);
  
  if (eatenCellI !== -1) {
    replaceEatenCell(playerColor, eatenCellI, board, graphBoard);
  }
  
  var tmpCell = board[startI];  
  board[startI] = board[endI];
  board[endI] = tmpCell;
  
  if (graphBoard) {
    var tmpGraphCell = graphBoard[startI];
    graphBoard[startI] = GRAPH_BOARD[endI];
    graphBoard[endI] = tmpGraphCell;
    
    drawCellOrPiece(graphBoard[startI], getXFromI(startI), getZFromI(startI));
    drawCellOrPiece(graphBoard[endI], getXFromI(endI), getZFromI(endI));
    
    changeTurn();
  }
  
  return true;
}

// heuristics

function countPieces(board) {
  var count = {
    black: 0,
    white: 0,
  };
  
  for (var i = 0; i < board.length; i++) {
    if (board[i] === BLACK_PIECE) {
      count.black++;
    } else if (board[i] === WHITE_PIECE) {
      count.white++;
    }
  }
  
  return count;
}

function getHeuristic(board) {
  var count = countPieces(board);
  
  return count.black - count.white;
}

// IA functions

function findNextPieceI(currI, board) {
  for (var i = currI + 1; i < board.length; i++) {
    if (board[i] === WORLD_COLOR) {
      return i;
    }
  }
  
  return -2;
}

function findRandomInBestSolutions(moves) {
  if (moves.length === 1) {
    return moves[0];
  }
  
  return moves[randomIntAtoB(0, moves.length - 1)];
}

function findBestMoveFromTab(moves) {
  if (!moves || !moves.length) {
    return null;
  }
  
  var bestScore = moves[0].score;
  
  for (var i = 1; i < moves.length; i++) {
    if (moves[i].score > bestScore) {
      bestScore = moves[i].score;
    }
  }
  
  var bestMoves = moves.filter(function (move) {
    return move.score === bestScore;
  });
  
  return findRandomInBestSolutions(bestMoves);
}

function tryMove(board, moves, startI, endI) {
  if (isCorrectPos(getXFromI(startI)) && isCorrectPos(getZFromI(startI))
    && isCorrectPos(getXFromI(endI)) && isCorrectPos(getZFromI(endI))
    && move(WORLD_COLOR, startI, endI, board)) {
    moves.push({
      startI: startI,
      endI: endI,
      score: getHeuristic(board),
    });
    
    move(WORLD_COLOR, endI, startI, board, null, true);
  }
}

function findBestMoveForPiece(startI, board) {
  var moves = [];
  
  var startX = getXFromI(startI);
  var startZ = getZFromI(startI);
  
  var endZ = startZ + 1;
  var endX = startX + 1;
  var endI = getIFromXZ(endX, endZ);
  
  tryMove(board, moves, startI, endI);
  
  endX = startX - 1;
  endI = getIFromXZ(endX, endZ);
  
  tryMove(board, moves, startI, endI);
  
  endZ = startZ + 2;
  endX = startX + 2;
  endI = getIFromXZ(endX, endZ);
  
  tryMove(board, moves, startI, endI);
  
  endX = startX - 2;
  endI = getIFromXZ(endX, endZ);
  
  tryMove(board, moves, startI, endI);
  
  return findBestMoveFromTab(moves);
}

// --- play functions -----------------------------------

function playMind(a) {
  if (a === -1) {
    return -1;
  }
  
  var startX = a.startX;
  var startZ = a.startZ;
  var endX = a.endX;
  var endZ = a.endZ;
  
  if (startX === undefined || startZ === undefined || endX == undefined || endZ === undefined) {
    alert("The mind is supposed to provide an object containing: startX, startZ, endX, endZ");
  }
  
  var startI = getIFromXZ(startX, startZ);
  var endI = getIFromXZ(endX, endZ);
  
  return move(MIND_COLOR, startI, endI, BOARD, GRAPH_BOARD);
}

function playWorld() {
  var COPY_BOARD = BOARD.slice();
  var currI = -1;
  var possibleMoves = [];
  var currBestMove;
  var bestMoveOfAll;
  
  while (currI !== -2) {
    currI = findNextPieceI(currI, COPY_BOARD);
    if (currI !== -2) {
      currBestMove = findBestMoveForPiece(currI, COPY_BOARD);
      if (currBestMove) {
        possibleMoves.push(currBestMove);
      }
    }
  }
  
  bestMoveOfAll = findBestMoveFromTab(possibleMoves);
  if (bestMoveOfAll) {
    return move(WORLD_COLOR, bestMoveOfAll.startI, bestMoveOfAll.endI, BOARD, GRAPH_BOARD);
  }
  
   return -1;
}

//--- public functions / interface / API ----------------------------------------------------------

this.endCondition;			// If set to true, run will end. 

this.newRun = function() 
{
  this.endCondition = false;
  turn = MIND_COLOR;
  noMoreMoves = false;

 	initBoard();
  
  if ( true  )
  {
    if (show3d) {
     BOXHEIGHT = squaresize;
     threeworld.init3d(startRadiusConst, maxRadiusConst, SKYCOLOR); 	
    }	     
    else {
     BOXHEIGHT = 1;
     threeworld.init2d(startRadiusConst, maxRadiusConst, SKYCOLOR); 		     
    }

    initSkybox();
    initGraphBoard();

    loadTextures();	
  }
};

this.getState = function()
{
 return BOARD.slice();
};

this.takeAction = function ( a )
{
  var firstTurn = true;
  console.log(a);
  
  if (true)
   updateStatusBefore(a);			// show status line before moves 

  setTimeout(function() {  // wait for the textures to initialize
    if (turn === MIND_COLOR) {
      if (playMind(a) === -1) {
        noMoreMoves = true;
      }
    } else if (turn === WORLD_COLOR) {
      if (playWorld() === -1) {
        noMoreMoves = true;
      }
    }
    
    if (checkWin()) {
      this.endCondition = true;
      
      if ( true  )
       {
        if ( this.endCondition )
          $("#user_span6").html( " &nbsp; <font color=green> The winner is: <B>" + winner + "</B>. </font>   "  );
        else
          $("#user_span6").html( " &nbsp; <font color=red> <B> No more moves available. </B> </font>   "  );
       }
    }
    
    firstTurn = false;
  }, firstTurn ? 1000 : 0);

};

this.getScore = function()
{
 return ((winner === 'PacMan (Mind)') * 100);
};

this.endRun = function()
{
 if ( true  )
 {
  if ( this.endCondition )
    $("#user_span6").html( " &nbsp; <font color=green> The winner is: <B>" + winner + "</B>. </font>   "  );
  else
    $("#user_span6").html( " &nbsp; <font color=red> <B> No more moves available. </B> </font>   "  );
 }
};

}

//---- end of World class -------------------------------------------------------