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> 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 = " pos = {" + s.y.toString() + 'x' + s.x.toString() + ") <BR> ";
$("#user_span4").html(status);
var score = self.getScore();
status = " Bad steps: " + badsteps +
" Good steps: " + goodsteps +
" 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(" <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;
}
};