Code viewer for World: Maze


// =============================================================================================
// 3d-effect Maze World(really a 3-D problem)
// Jean Gamain, 2016.
//
// This more complex World shows:
// - Skybox
// - Internal maze(randomly drawn each time)
// - Music/audio
// - User keyboard control(clone this and comment out Mind actions to see)
// =============================================================================================


// =============================================================================================
// Scoring:
// Bad steps = steps where enemy is within one step of agent.
// Good steps = steps where enemy is further away.
// Score = good steps as percentage of all steps.
//
// There are situations where agent is trapped and cannot move.
// If this happens, you score zero.
//
// Scoring on the server side is done by taking average of n runs.
// Runs where you get trapped and score zero can seriously affect this score.
// =============================================================================================

// World must define these:

const	 	CLOCKTICK 	= 100;					// speed of run - move things every n milliseconds
const		MAXSTEPS 	= 4000;					// length of a run before final score

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

const MAZESIZE = [20, 30]; // maze size
const BOXHEIGHT = 100;		// 3d or 2d box height

const SKYCOLOR 	= 0xddffdd;				// a number, not a string
const BLANKCOLOR 	= SKYCOLOR ;			// make objects this color until texture arrives(from asynchronous file read)
const MAXPOSX = MAZESIZE[0] * BOXHEIGHT;
const MAXPOSY = MAZESIZE[1] * BOXHEIGHT;
const MAXPOS = (MAXPOSX + MAXPOSY) / 2;

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

// contents of a grid square

//---- start of World class -------------------------------------------------------

function World() {
  var MAZE;

  var AI = {
    x: 0,
    y: 0,
  };

  var badsteps;
  var goodsteps;
  var step;

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




// regular "function" syntax means private functions:

  function translateX(x)
  {
    return(x -(MAXPOSX / 2));
  }
  function translateY(y)
  {
    return(y -(MAXPOSY / 2));
  }

  //--- 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
  }

  /*
   // --- alternative skyboxes: ------------------------------

   // space skybox, credit:
   // http://en.spaceengine.org/forum/21-514-1
   // x,y,z labelled differently

   var materialArray = [
   (new THREE.MeshBasicMaterial({ map: THREE.ImageUtils.loadTexture("/uploads/starter/sky_pos_z.jpg"), side: THREE.BackSide })),
   (new THREE.MeshBasicMaterial({ map: THREE.ImageUtils.loadTexture("/uploads/starter/sky_neg_z.jpg"), side: THREE.BackSide })),
   (new THREE.MeshBasicMaterial({ map: THREE.ImageUtils.loadTexture("/uploads/starter/sky_pos_y.jpg"), side: THREE.BackSide })),
   (new THREE.MeshBasicMaterial({ map: THREE.ImageUtils.loadTexture("/uploads/starter/sky_neg_y.jpg"), side: THREE.BackSide })),
   (new THREE.MeshBasicMaterial({ map: THREE.ImageUtils.loadTexture("/uploads/starter/sky_pos_x.jpg"), side: THREE.BackSide })),
   (new THREE.MeshBasicMaterial({ map: THREE.ImageUtils.loadTexture("/uploads/starter/sky_neg_x.jpg"), side: THREE.BackSide })),
   ];

   // urban photographic skyboxes, credit:
   // http://opengameart.org/content/urban-skyboxes

   var materialArray = [
   (new THREE.MeshBasicMaterial({ map: THREE.ImageUtils.loadTexture("/uploads/starter/posx.jpg"), side: THREE.BackSide })),
   (new THREE.MeshBasicMaterial({ map: THREE.ImageUtils.loadTexture("/uploads/starter/negx.jpg"), side: THREE.BackSide })),
   (new THREE.MeshBasicMaterial({ map: THREE.ImageUtils.loadTexture("/uploads/starter/posy.jpg"), side: THREE.BackSide })),
   (new THREE.MeshBasicMaterial({ map: THREE.ImageUtils.loadTexture("/uploads/starter/negy.jpg"), side: THREE.BackSide })),
   (new THREE.MeshBasicMaterial({ map: THREE.ImageUtils.loadTexture("/uploads/starter/posz.jpg"), side: THREE.BackSide })),
   (new THREE.MeshBasicMaterial({ map: THREE.ImageUtils.loadTexture("/uploads/starter/negz.jpg"), side: THREE.BackSide })),
   ];

   */
// --- asynchronous load textures from file ----------------------------------------

  function loadTextures()
  {

    var loader1 = new THREE.TextureLoader();
    loader1.load('/uploads/starter/door.jpg',		function(thetexture) {
      thetexture.minFilter = THREE.LinearFilter;
      paintWalls(new THREE.MeshBasicMaterial({ map: thetexture }));
    });

  }

// --- add fixed objects ----------------------------------------



  function initMaze()		// graphical run only
  {
    function createWall(x, y, side, color) {
      var plane = new THREE.PlaneGeometry(BOXHEIGHT, BOXHEIGHT);
      var wall = new THREE.Mesh(plane);
      wall.material.color.setHex(color);
      wall.material.side = THREE.DoubleSide;
      wall.material.alphaTest = 0.5;
      wall.position.x = translateX((x + ((side) ? -0.5 : 0)) * BOXHEIGHT);
      wall.position.z = translateY((y + ((side) ? -0.5 : 0)) * BOXHEIGHT);
      if (side)
        wall.rotation.y = Math.PI / 2;
      threeworld.scene.add(wall);
    }

    MAZE.map(function (d, x) {
      if (!MAZE[x][0].door[0].open)
        createWall(x, -1, 0, 0xFF0000);
    });
    MAZE.map(function (d, x) {
      MAZE[x].map(function (dd, y) {
        if (!MAZE[x][y].door[3].open)
          createWall(x, y, 1, 0x00FF00);
        if (!MAZE[x][y].door[2].open)
          createWall(x, y, 0, 0x0000FF);
      });
    });
    MAZE[0].map(function (d, y) {
      if (!MAZE[MAZE.length - 1][y].door[1].open)
        createWall(MAZE.length, y, 1, 0xFCFCFC);
    });
    // ground
    var plane = new THREE.PlaneGeometry(BOXHEIGHT * MAZESIZE[0], BOXHEIGHT * MAZESIZE[1]);
    var ground = new THREE.Mesh(plane);
    ground.material.color.setHex(0x4286f4);
    ground.material.side = THREE.DoubleSide;
    ground.position.x = BOXHEIGHT * -0.5;
    ground.position.z = -BOXHEIGHT;
    ground.position.y = -BOXHEIGHT * 0.5;
    ground.rotation.x = Math.PI / 2;
    threeworld.scene.add(ground);
    // end
    plane = new THREE.PlaneGeometry(BOXHEIGHT, BOXHEIGHT);
    var end = new THREE.Mesh(plane);
    end.material.color.setHex(0x00FF00);
    end.material.side = THREE.DoubleSide;
    end.position.x = translateX(MAZE.end.x * BOXHEIGHT);
    end.position.z = translateY((MAZE.end.y - 0.5) * BOXHEIGHT);
    end.position.y = -BOXHEIGHT * 0.5;
    end.rotation.x = Math.PI / 2;
    threeworld.scene.add(end);
  }

  // --- agent functions -----------------------------------

  function drawAgent()
  {
    AI.model.position.x = translateX(AI.x * BOXHEIGHT);
    AI.model.position.y = BOXHEIGHT * 0.25;
    AI.model.position.z = translateY((AI.y - 0.5) * BOXHEIGHT);
    threeworld.scene.add(AI.model);
    threeworld.follow.copy(AI.model.position);
  }


  function initLogicalAgent()
  {
    AI = {
      x: Math.floor(Math.random() * MAZESIZE[0]),
      y: Math.floor(Math.random() * MAZESIZE[1]),
    };
  }

  function initThreeAgent()
  {
    var shape = new THREE.BoxGeometry(BOXHEIGHT, MAZESIZE[0], BOXHEIGHT);
    AI.model = new THREE.Mesh(shape);
    AI.model.material.color.setHex(BLANKCOLOR);
    drawAgent();
  }


  function moveLogicalAgent(a)			// this is called by the infrastructure that gets action a from the Mind
  {
    AI.x = a.x;
    AI.y = a.y;
    if (MAZE.end.x === AI.x && MAZE.end.y === AI.y)
      this.endCondition = true;
  }

  // --- score: -----------------------------------


  function badstep(a)
  {
    var vec = {
      x: AI.x - a.x,
      y: AI.y - a.y,
    };
    var axe = labyrinthe.axis.indexOf(vec);
    return (axe !== -1 && MAZE[AI.x][AI.y].door[axe].open);
  }


  function updateStatusBefore()
// this is called before anyone has moved on this step, agent has just proposed an action
// update status to show old state and proposed move
  {
    var s = self.getState();
    var status = " Step: <b> " + step + " </b> &nbsp; pos = {" + s.y.toString() + 'x' + s.x.toString() + ")";

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


  function updateStatusAfter()		// agent and enemy have moved, can calculate score
  {
    // new state after both have moved
    var s = self.getState();
    var status = " &nbsp; pos = {" + s.y.toString() + 'x' + s.x.toString() + ") <BR> ";
    $("#user_span4").html(status);
    var score = self.getScore();

    status = "   Bad steps: " + badsteps +
      " &nbsp; Good steps: " + goodsteps +
      " &nbsp; Score: " + score.toFixed(2) + "% ";

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

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

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

  this.newRun = function()
  {

    //(subtle bug) must reset variables like these inside newRun(in case do multiple runs)
    this.endCondition = false;
    badsteps = 0;
    goodsteps = 0;
    step = 0;

    //init
    MAZE = labyrinthe.create(MAZESIZE[0], MAZESIZE[1]);
    var endAxe = (Math.random() > 0.5);
    MAZE.end = {
      x: (endAxe) ? ((Math.random() > 0.5) ? MAZESIZE[0] : -1) : Math.floor(Math.random() * MAZESIZE[0]),
      y: (!endAxe) ? ((Math.random() > 0.5) ? MAZESIZE[1] : -1) : Math.floor(Math.random() * MAZESIZE[1]),
    };
    // open a wall to exit the maze
    MAZE
      [((endAxe) ? ((MAZE.end.x == -1) ? 0 : MAZESIZE[0] - 1) : MAZE.end.x)]
      [((!endAxe) ? ((MAZE.end.y == -1) ? 0 : MAZESIZE[1] - 1) : MAZE.end.y)]
      .door[((endAxe) ? ((MAZE.end.x == -1) ? 3 : 1) : ((MAZE.end.y == -1) ? 0 : 2))].open = true;
    initLogicalAgent();

    // for graphical runs only:
    if (true)
    {
      threeworld.init3d(startRadiusConst, maxRadiusConst, 0xddffdd);
      initSkybox();
      //initMusic();
      initMaze();
      // Set up objects first:
      initThreeAgent();
      //loadTextures();
    }

  };

  this.getState = function()
  {
    return({
      x: AI.x,
      y: AI.y,
      env: (MAZE[AI.x] && MAZE[AI.x][AI.y]) ? MAZE[AI.x][AI.y].door : [],
    });
  };

  this.takeAction = function(a)
  {
    step++;
    if(true)
      updateStatusBefore(a);			// show status line before moves
    if (badstep(a))
      badsteps++;
    else {
      goodsteps++;
      moveLogicalAgent(a);
    }
    if(true)
    {
      drawAgent();
      updateStatusAfter();			// show status line after moves
    }
    //musicPause();
    //soundAlarm();
  };



  this.endRun = function()
  {
    if(true)
    {
      //musicPause();
      $("#user_span6").html(" &nbsp; <font color=red> <B> Run over. </B> </font>   ");
    }
  };


  this.getScore = function()
  {
    return((goodsteps / step) * 100);
  };


}

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

// --- music and sound effects ----------------------------------------
// credits:
// http://www.dl-sounds.com/royalty-free/defense-line/
// http://soundbible.com/1542-Air-Horn.html

function initMusic()
{
  // put music element in one of the spans
  var x = "<audio  id=theaudio  src=/uploads/starter/Defense.Line.mp3   autoplay loop> </audio>" ;
  $("#user_span1").html(x);
}


function musicPlay()
{
  // jQuery does not seem to parse pause() etc. so find the element the old way:
  document.getElementById('theaudio').play();
}


function musicPause()
{
  document.getElementById('theaudio').pause();
}


function soundAlarm()
{
  var x = "<audio    src=/uploads/starter/air.horn.mp3   autoplay  > </audio>";
  $("#user_span2").html(x);
}



var labyrinthe = {
  axis: [
    [0, -1],	[1, 0], [0, 1], [-1, 0]
  ],
  create: function (x, y) {
    var laby = new Array();
    for (var posX = 0; posX < x; posX++) {
      laby[posX] = Array();
      for (var posY = 0; posY < y; posY++) {
        laby[posX][posY] = {
          door: [
            Object({ open: false }),
            Object({ open: false }), Object({ open: false }),
            Object({ open: false }),
            /* (!posX) ? new Object({ open: false }) : laby[posX - 1][posY].door[2],
             new Object({ open: false }), new Object({ open: false }),
             (!posY) ? new Object({ open: false }) : laby[posX][posY - 1].door[1],*/
          ],
          boundDoor: [],
        };
      }
    }
    this.setRandomValue(laby);
    for (var i = 0; i < (laby.length * laby[0].length - 1); i += (labyrinthe.generate(laby, Math.floor(Math.random() * laby.length), Math.floor(Math.random() * laby[0].length))) ? 1 : 0);
    return laby;
  },
  propagate: function(laby, x, y, value) {
    laby[x][y].value = value;
    this.axis.forEach(function(axe, i) {
      var pos = { x: x + axe[0], y: y + axe[1] };
      if (typeof(laby[x][y].door[i]) != 'undefined' &&
        laby[x][y].door[i].open &&
        pos.x >= 0 && pos.x < laby.length &&
        pos.y >= 0 && pos.y < laby[0].length &&
        !laby[pos.x][pos.y].visited &&
        laby[pos.x][pos.y].value != laby[x][y].value)
        this.propagate(laby, pos.x, pos.y, value);
    }, this);
  },
  generate: function (laby, x, y, rootNode) {
    laby[x][y].visited = true;
    var possiblePath = Array();
    var possibleDoorToOpen = Array();
    this.axis.forEach(function(axe, i) {
      var pos = { x: x + axe[0], y: y + axe[1] };
      if (typeof(laby[x][y].door[i]) != 'undefined' &&
        pos.x >= 0 && pos.x < laby.length &&
        pos.y >= 0 && pos.y < laby[0].length &&
        !laby[pos.x][pos.y].visited) {
        if (laby[x][y].door[i].open)
          possiblePath.push(i);
        else if (laby[pos.x][pos.y].value !== laby[x][y].value)
          possibleDoorToOpen.push(i);
      }
    });
    if (possiblePath.length >= 1 && Math.floor(Math.random() * (possiblePath.length + possibleDoorToOpen.length)) <= possiblePath.length) {
      while (possiblePath.length) {
        var randomPath = Math.floor(Math.random() * possiblePath.length);
        if (this.generate(laby, this.axis[possiblePath[randomPath]][0] + x, this.axis[possiblePath[randomPath]][1] + y)) {
          laby[x][y].visited = false;
          return true;
        }
        possiblePath = possiblePath.slice(0, randomPath).concat(possiblePath.slice(randomPath + 1));
      }
    }
    if (possibleDoorToOpen.length) {
      var randomDoor = Math.floor(Math.random() * possibleDoorToOpen.length);
      this.propagate(laby, this.axis[possibleDoorToOpen[randomDoor]][0] + x, this.axis[possibleDoorToOpen[randomDoor]][1] + y, laby[x][y].value);
      laby[x][y].door[possibleDoorToOpen[randomDoor]].open = true;
      laby[x + this.axis[possibleDoorToOpen[randomDoor]][0]][y + this.axis[possibleDoorToOpen[randomDoor]][1]].door[(possibleDoorToOpen[randomDoor] + 2) % 4].open = true;
      laby[x][y].visited = false;
      return true;
    }
    laby[x][y].visited = false;
    return false;
  },
  setRandomValue: function (laby) {
    var values = Array();
    for (var i = 0; i < (laby.length * laby[0].length); i++)
      values.push(i);
    laby.forEach(function(x) {
      x.forEach(function(c) {
        var randomIndex = Math.floor(Math.random() * values.length);
        c.value = values[randomIndex];
        c.visited = false;
        values = values.slice(0, randomIndex).concat(values.slice(randomIndex + 1));
      });
    });
  },
  print: function(laby) {
    var str = "n";
    laby[0].map(function (d, x) {
      str += (!laby[x][0].door[0].open) ? ' _' : '  ';
    });
    str += "n";
    laby.map(function (d, y) {
      laby[y].map(function (dd, x) {
        str += (!laby[x][y].door[3].open) ? '|' : ' ';
        str += (!laby[x][y].door[2].open) ? '_' : ' ';
      });
      str += (laby[laby.length - 1][y].door[1].open) ? ' ' : '|' + "n";
    });
    return str;
  }
};