/*
README:
Assignment Grading:
Author: Bernard O'Connor
Student Number: 14367821
I would like for this world and the three minds I have written for it to be all considered in
the grading.
My minds can be found at the following address, ranked by importance:
1) AI VS AI: http://ab.computing.dcu.ie/sys/mind.php?mind=4719572275
2) Human VS AI: http://ab.computing.dcu.ie/sys/mind.php?mind=9140078057
3) Human VS Human: http://ab.computing.dcu.ie/sys/mind.php?mind=4958494060
Overview:
This world is a maze where two agents will fight each other, while also trying to find various pickups have different
effects. This world can be run in different modes depending on which AI you choose to run with the world. The
different modes are:
1) AI VS AI
2) Human VS AI
3) Human VS Human
Current Control Scheme:
Action: Agent Controls: Enemy Controls
Move Up up arrow w
Move Left left arrow a
Move Down down arrow s
Move Right right arrow d
Attack Insert key (num 0) spacebar
Current Pickup Legend:
Type: Shape: Ability:
Portal purple sphere Teleports the agent to a random non-occupied location on the map
Damage pickup red octahedron Increases the attack damage of the agent
health pickup green dodecahedron Increases the health of the agent
AI Behaviour States:
There are 4 behaviour states that an AI agent can be in.
1) Damage Hunting:
This is the starting behaviour of the AI. The AI will avoid their opponent until they pick up at least
one Damage Pickup, simulating the AI desperately searching for a starting weapon to defend themself with.
They will also keep an eye out for Health Pickups, as they are always useful.
2) Opponent Hunting:
After an AI has found a damage pickup, they will start hunting down their opponent.
3) Fighting:
Once the AI has either found their opponent after they have been hunting them, or if the AI has been
hunted down and trapped by their opponent, they will then fight their opponent. If their health drops
below 25 then if they are not trapped they will run away from their opponent in search of Health. If they are
trapped then they will fight to the death.
4) Health Hunting:
Once an AI's health has fallen below 25 they will look for health pickups if they are able. Once they have
found a health pickup they will then enter either the Damage Hunting state or the Opponent Hunting state,
depending on if they have previously found a damage pickup before their health had dropped below 25.
One thing to note is that if both AI manage to get into this mode, they will both run away from each other, and
if there is no health left on the map, they will not fight each other. This is not an issue however, as both
agents will still recieve a score based on how much damage they dealt and how much health they have left.
The AI is able to navigate through the maze through the use of a "line of sight" system in conjunction with the
above behaviour states. A "line of sight" is a line that is three blocks wide and extends from each direction
of the AI until their view is blocked by either a wall or by the opposing agent. The AI bases its action decision
making both off of the information it is able to see from its "line of sight" and on certain other factors such as
its current health and attack damage.
Scoring System:
Each AI's current score is their remaining health + how much damage they inflict on
their opponent. If they survive the run they get +100 points. If they die
they lose 50 points.
There will be some maximum amount of turns to end the game if neither AI dies.
For the overall world scoreboard, the highest of the two scores produced in this run is given.
Combat System:
When an agent is in range of their opponent and is in the FIGHTING behaviour, they will subtract their attack damage
from the opponent's health.
Future Improvements:
I have attempted to write this assignment in such a way where it can be expanded upon in the future. Some refactoring
will have to be done at a later stage in some areas to improve upon this.
One example of an improvement I would like to make is to add a dice roll system to the amount of damage dealt by
an agent, so that it is a variable number. The damage pickups would then serve to improve this diceroll number
by maybe providing an extra damage dice to roll, or just to add a fixed amount of damage to the dice.
Another improvement would be to let the AI use a built up memory of the maze in their decision making rather than just
on their line of sight.
*/
// World must define these:
const CLOCKTICK = 100; // speed of run - move things every n milliseconds
const MAXSTEPS = 1000; // length of a run before final score
const SCREENSHOT_STEP = 50;
//---- global constants: -------------------------------------------------------
const GRIDSIZE = 23; // number of squares along side of world. THIS IS A FIXED SIZE OF 23 FOR THIS MANUALLY MADE maze!!!
const squaresize = 100; // size of square in pixels
const MAXPOS = GRIDSIZE * squaresize; // length of one side in pixels
const SKYCOLOR = 0xddffdd; // a number, not a string
const BLANKCOLOR = SKYCOLOR ; // make objects this color until texture arrives (from asynchronous file read)
const show3d = false; // Switch between 3d and 2d view (both using Three.js)
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
//--- Mind can pick one of these actions -----------------
const ACTION_LEFT = 0;
const ACTION_RIGHT = 1;
const ACTION_UP = 2;
const ACTION_DOWN = 3;
const ACTION_STAYSTILL = 4;
const ACTION_ATTACK = 5;
// 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 GRID_BLANK = 0;
const GRID_WALL = 1;
const GRID_PORTAL = 2;
const GRID_DAMAGE_PICKUP = 3;
const GRID_HEALTH_PICKUP = 4;
const GRID_OPPONENT_OCCUPIED = 5; //this is only set in an AI's lineOfSight 2d array
//pickup modifiers
const DAMAGE_MODIFIER = 5;
const HEALTH_MODIFIER = 25;
// --- some useful random functions -------------------------------------------
function randomfloatAtoB ( A, B )
{
return ( A + ( Math.random() * (B-A) ) );
}
function randomintAtoB ( A, B )
{
return ( Math.round ( randomfloatAtoB ( A, B ) ) );
}
function randomBoolean()
{
if ( Math.random() < 0.5 ) { return false; }
else { return true; }
}
//---- 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 GRID = new Array(GRIDSIZE); // can query GRID about whether squares are occupied, will in fact be initialised as a 2D array
var mazeObjects = new Array (GRIDSIZE); //this holds every THREE.js object in the maze (except for the AI), will in fact be initialised as a 2D array
var aiHasDied = false; //When this is set true, do one final score calculation, then set this.endCondition to true.
var agentMap = { //the agent AIMap
'name': 'My name is Agent', //debug purposes
'i': null,
'j': null,
'oldI':null,
'oldJ':null,
'attackDamage': 1,
'attackRange': 1,
'opponentInRange': false,
'totalDamageDealt': 0,
'health': 100,
'mesh': null,
'score': 0,
'lineOfSight': new Array(GRIDSIZE),
'trapped':false
};
var enemyMap = { //the enemy AIMap
'name': 'My name is Enemy', //debug purposes
'i': null,
'j': null,
'oldI':null,
'oldJ':null,
'attackDamage': 1,
'attackRange': 1,
'opponentInRange': false,
'totalDamageDealt': 0,
'health': 100,
'mesh': null,
'score': 0,
'lineOfSight': new Array(GRIDSIZE),
'trapped':false
};
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 initGrid()
{
for (var i = 0; i < GRIDSIZE ; i++)
{
GRID[i] = new Array(GRIDSIZE); // each element is an array
mazeObjects[i] = new Array(GRIDSIZE); //also make the array that tracks maze objects 2d
agentMap['lineOfSight'][i] = new Array(GRIDSIZE); //also make the agent's lineOfSight array 2d
enemyMap['lineOfSight'][i] = new Array(GRIDSIZE); //also make the enemy's lineOfSight array 2d
for (var j = 0; j < GRIDSIZE ; j++)
{
GRID[i][j] = GRID_BLANK ;
}
}
}
function occupied ( i, j ) // is this square occupied
{
if ( ( enemyMap['i'] == i ) && ( enemyMap['j'] == j ) ) return true; // variable objects
if ( ( agentMap['i'] == i ) && ( agentMap['j'] == j ) ) return true;
if ( GRID[i][j] == GRID_WALL ) return true; // fixed objects and pickups
return false;
}
// logically, coordinates are: y=0, x and z all positive (no negative)
// logically my dimensions are all positive 0 to MAXPOS
// to centre everything on origin, subtract (MAXPOS/2) from all dimensions
function translate ( x )
{
return ( x - ( MAXPOS/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
}
// This does the file read the old way using loadTexture.
// todo Change to asynchronous TextureLoader. A bit complex:
// Make blank skybox. Start 6 asynch file loads to call 6 return functions.
// Each return function checks if all 6 loaded yet. Once all 6 loaded, paint the skybox.
// --- asynchronous load textures from file ----------------------------------------
// credits:
// http://commons.wikimedia.org/wiki/File:Old_door_handles.jpg?uselang=en-gb
// https://commons.wikimedia.org/wiki/Category:Pac-Man_icons
// https://commons.wikimedia.org/wiki/Category:Skull_and_crossbone_icons
// http://en.wikipedia.org/wiki/File:Inscription_displaying_apices_(from_the_shrine_of_the_Augustales_at_Herculaneum).jpg
// loader return can call private function
function loadTextures()
{
/*
var loader2 = new THREE.TextureLoader();
loader2.load ( '/uploads/starter/latin.jpg', function ( thetexture ) {
thetexture.minFilter = THREE.LinearFilter;
paintMaze ( new THREE.MeshBasicMaterial( { map: thetexture } ) );
} );
*/
var loader3 = new THREE.TextureLoader();
loader3.load ( '/uploads/starter/pacman.jpg', function ( thetexture ) {
thetexture.minFilter = THREE.LinearFilter;
agentMap['mesh'].material = new THREE.MeshBasicMaterial( { map: thetexture } );
} );
var loader4 = new THREE.TextureLoader();
loader4.load ( '/uploads/starter/ghost.3.png', function ( thetexture ) {
thetexture.minFilter = THREE.LinearFilter;
enemyMap['mesh'].material = new THREE.MeshBasicMaterial( { map: thetexture } );
} );
}
// --- add fixed objects ----------------------------------------
//This function will create the entire logical maze, including all pickups and portals
function initLogicalMaze()
{
//This function is hard-coded for a fixed maze of length 23 across and down.
// I will manually make rows 0-11. Rows 12-22 will be a mirror image of rows 0-10.
// Note by default each square is already a GRID_BLANK due to initGrid()
//row 0
for(var i = 0; i < GRIDSIZE; i++)
{
GRID[i][0] = GRID_WALL;
}
//row 1
GRID[0][1] = GRID_WALL;
GRID[1][1] = GRID_PORTAL;
GRID[4][1] = GRID_WALL;
GRID[10][1] = GRID_WALL;
GRID[12][1] = GRID_WALL;
GRID[21][1] = GRID_PORTAL;
GRID[22][1] = GRID_WALL;
//row 2
GRID[0][2] = GRID_WALL;
GRID[2][2] = GRID_WALL;
for(i = 6; i <= 22; i += 2)
{
GRID[i][2] = GRID_WALL;
}
//row 3
GRID[0][3] = GRID_WALL;
GRID[2][3] = GRID_WALL;
GRID[3][3] = GRID_WALL;
GRID[5][3] = GRID_WALL;
GRID[6][3] = GRID_WALL;
GRID[8][3] = GRID_WALL;
GRID[9][3] = GRID_WALL;
for(i = 10; i <= 22; i += 2)
{
GRID[i][3] = GRID_WALL;
}
//row 4
GRID[0][4] = GRID_WALL;
GRID[5][4] = GRID_WALL;
GRID[8][4] = GRID_WALL;
GRID[22][4] = GRID_WALL;
//row 5
GRID[0][5] = GRID_WALL;
GRID[1][5] = GRID_WALL;
GRID[3][5] = GRID_WALL;
GRID[5][5] = GRID_WALL;
GRID[7][5] = GRID_WALL;
GRID[8][5] = GRID_WALL;
for(i = 10; i <= 16; i++)
{
GRID[i][5] = GRID_WALL;
}
GRID[17][5] = GRID_HEALTH_PICKUP;
GRID[18][5] = GRID_WALL;
GRID[19][5] = GRID_WALL;
GRID[20][5] = GRID_WALL;
GRID[22][5] = GRID_WALL;
//row 6
GRID[0][6] = GRID_WALL;
GRID[3][6] = GRID_WALL;
GRID[5][6] = GRID_WALL;
GRID[10][6] = GRID_WALL;
GRID[14][6] = GRID_WALL;
GRID[16][6] = GRID_WALL;
GRID[17][6] = GRID_WALL;
GRID[18][6] = GRID_WALL;
GRID[22][6] = GRID_WALL;
//row 7
GRID[0][7] = GRID_WALL;
GRID[2][7] = GRID_WALL;
GRID[3][7] = GRID_WALL;
GRID[7][7] = GRID_WALL;
GRID[9][7] = GRID_WALL;
GRID[10][7] = GRID_WALL;
GRID[12][7] = GRID_WALL;
GRID[14][7] = GRID_WALL;
GRID[18][7] = GRID_WALL;
GRID[19][7] = GRID_DAMAGE_PICKUP;
GRID[20][7] = GRID_WALL;
GRID[22][7] = GRID_WALL;
//row 8
GRID[0][8] = GRID_WALL;
GRID[2][8] = GRID_DAMAGE_PICKUP;
GRID[3][8] = GRID_WALL;
GRID[4][8] = GRID_WALL;
GRID[5][8] = GRID_WALL;
GRID[7][8] = GRID_WALL;
GRID[12][8] = GRID_WALL;
GRID[14][8] = GRID_WALL;
GRID[16][8] = GRID_WALL;
GRID[18][8] = GRID_WALL;
GRID[19][8] = GRID_WALL;
GRID[20][8] = GRID_WALL;
GRID[22][8] = GRID_WALL;
//row 9
GRID[0][9] = GRID_WALL;
GRID[2][9] = GRID_WALL;
GRID[4][9] = GRID_HEALTH_PICKUP;
GRID[5][9] = GRID_WALL;
for(i = 7; i <= 10; i++)
{
GRID[i][9] = GRID_WALL;
}
GRID[12][9] = GRID_WALL;
GRID[16][9] = GRID_WALL;
GRID[22][9] = GRID_WALL;
//row 10
GRID[0][10] = GRID_WALL;
GRID[4][10] = GRID_WALL;
GRID[8][10] = GRID_WALL;
GRID[10][10] = GRID_WALL;
GRID[12][10] = GRID_WALL;
GRID[13][10] = GRID_WALL;
GRID[15][10] = GRID_WALL;
GRID[17][10] = GRID_WALL;
GRID[18][10] = GRID_WALL;
GRID[20][10] = GRID_WALL;
GRID[21][10] = GRID_WALL;
GRID[22][10] = GRID_WALL;
//row 11. This is the middle row (it will not be mirrored, only 0-10 will be mirrored)
for(var i = 0; i <= 3; i++)
{
GRID[i][11] = GRID_WALL;
}
GRID[4][11] = GRID_PORTAL;
GRID[6][11] = GRID_WALL;
GRID[22][11] = GRID_WALL;
//mirror the remaining rows.
var jOld = 10;
for(var jNew = 12; jNew < GRIDSIZE; jNew++)
{
for(i = 0; i < GRIDSIZE; i++)
{
GRID[i][jNew] = GRID[i][jOld];
}
jOld--;
}
}
function initThreeMaze()
{
var geometry;
var material;
var theMesh;
for (var i = 0; i < GRIDSIZE ; i++) {
for (var j = 0; j < GRIDSIZE; j++) {
if (GRID[i][j] == GRID_WALL) {
geometry = new THREE.BoxGeometry(squaresize, BOXHEIGHT, squaresize);
material = new THREE.MeshBasicMaterial({color: 0x008000})
theMesh = new THREE.Mesh(geometry, material);
// theMesh.material.color.setHex(BLANKCOLOR);
theMesh.position.x = translate(i * squaresize);
theMesh.position.z = translate(j * squaresize);
theMesh.position.y = 0;
threeworld.scene.add(theMesh);
mazeObjects[i][j] = theMesh; // save it for later
}
else if (GRID[i][j] == GRID_PORTAL) {
geometry = new THREE.SphereGeometry(50, 32, 32);
material = new THREE.MeshBasicMaterial({color: 0x4d0099})
theMesh = new THREE.Mesh(geometry, material);
//theMesh.material.color.setHex(BLANKCOLOR);
theMesh.position.x = translate(i * squaresize);
theMesh.position.z = translate(j * squaresize);
theMesh.position.y = 0;
threeworld.scene.add(theMesh);
mazeObjects[i][j] = theMesh; // save it for later
}
else if (GRID[i][j] == GRID_DAMAGE_PICKUP) {
geometry = new THREE.OctahedronGeometry(50, 0);
material = new THREE.MeshBasicMaterial({color: 0x990000})
theMesh = new THREE.Mesh(geometry, material);
//theMesh.material.color.setHex(BLANKCOLOR);
theMesh.position.x = translate(i * squaresize);
theMesh.position.z = translate(j * squaresize);
theMesh.position.y = 0;
threeworld.scene.add(theMesh);
mazeObjects[i][j] = theMesh; // save it for later
}
else if (GRID[i][j] == GRID_HEALTH_PICKUP) {
geometry = new THREE.DodecahedronGeometry(50, 0);
material = new THREE.MeshBasicMaterial({color: 0x00ff00})
theMesh = new THREE.Mesh(geometry, material);
//theMesh.material.color.setHex(BLANKCOLOR);
theMesh.position.x = translate(i * squaresize);
theMesh.position.z = translate(j * squaresize);
theMesh.position.y = 0;
threeworld.scene.add(theMesh);
mazeObjects[i][j] = theMesh; // save it for later
}
}
}
}
// AI interactions with game world (i.e pickups and attacks)
function teleport(AIMap)
{
var i, j;
do
{
i = randomintAtoB(1,GRIDSIZE-2);
j = randomintAtoB(1,GRIDSIZE-2);
}
while ( occupied(i,j) && GRID[i][j] != GRID_BLANK); // search for empty square
//set old i and j to null
AIMap['oldI'] = null;
AIMap['oldJ'] = null;
//set new position
AIMap['i'] = i;
AIMap['j'] = j;
}
function addModifier(AIMap, modType)
{
if(modType == GRID_HEALTH_PICKUP)
{
AIMap['health'] += HEALTH_MODIFIER;
}
else if (modType == GRID_DAMAGE_PICKUP)
{
AIMap['attackDamage'] += DAMAGE_MODIFIER;
}
}
function removePickUp(i,j)
{
GRID[i][j] = GRID_BLANK;
threeworld.scene.remove(mazeObjects[i][j]);
}
function useBlockPickup(AIMap)
{
//need to keep track of initial i and j separately from the AI's i and j
var i = AIMap['i'];
var j = AIMap['j'];
if(i == null || j == null)
{
//do nothing, the ai is dead ,i.e position is null
}
else {
var blockType = GRID[i][j];
if (blockType == GRID_BLANK)
{
//Do nothing, the location contains no pickup
}
else {
if (blockType == GRID_PORTAL)
{
teleport(AIMap);
removePickUp(i, j);
}
else if (blockType == GRID_HEALTH_PICKUP || blockType == GRID_DAMAGE_PICKUP)
{
addModifier(AIMap, blockType);
removePickUp(i, j);
}
}
}
}
function targetInRange(attackerAIMap, targetAIMap)
{
//cant use distance formula, just judge range on either up and down range or left and right range, no diagonals
if(attackerAIMap['i'] == targetAIMap['i'])
{
if(Math.abs(attackerAIMap['j'] - targetAIMap['j']) <= attackerAIMap['attackRange'])
return true;
else
return false;
}
if(attackerAIMap['j'] == targetAIMap['j'])
{
if(Math.abs(attackerAIMap['i'] - targetAIMap['i']) <= attackerAIMap['attackRange'])
return true;
else
return false;
}
return false;
}
function attackTarget(attackerAIMap, targetAIMap)
{
if(attackerAIMap['opponentInRange'])
{
//todo introduce dice rolls into attack damage
var damageDealt;
if(!aiHasDied) //Stop bug where a dead ai can still attack between them dieing and the run ending
{
damageDealt = attackerAIMap['attackDamage'];
}
else
{
damageDealt = 0;
}
if(targetAIMap['health'] <= damageDealt)
{
attackerAIMap['totalDamageDealt'] += targetAIMap['health']; // Don't let the AI deal damage > the targets health
targetAIMap['health'] = 0; // don't allow negative health
aiHasDied = true; //target is dead
removeDeadAI(targetAIMap);
endRunProcedure(); //target has died, begin procedure to end the run
}
else
{
attackerAIMap['totalDamageDealt'] += damageDealt; //damage dealt is saved for scoring purposes
targetAIMap['health'] -= damageDealt;
}
}
}
function moveLogicalAI(AIMap, a)
{
var i = AIMap['i'];
var j = AIMap['j'];
if ( a == ACTION_LEFT ) i--;
else if ( a == ACTION_RIGHT ) i++;
else if ( a == ACTION_UP ) j--;
else if ( a == ACTION_DOWN ) j++;
else if (a == ACTION_STAYSTILL) {/* Do nothing*/}
if ( ! occupied(i,j) ) // if no obstacle then move, else just miss a turn
{
//Update old i and j first
AIMap['oldI'] = AIMap['i'];
AIMap['oldJ'] = AIMap['j'];
//move the ai the the new position
AIMap['i'] = i;
AIMap['j'] = j;
}
useBlockPickup(AIMap);
}
function takeTurnLogicalAI( thisAIMap, otherAIMap, a )
{
if(a == ACTION_ATTACK)
{
attackTarget(thisAIMap,otherAIMap);
}
else
{
moveLogicalAI(thisAIMap,a);
}
totalLineOfSight(thisAIMap); //get new line of sight at the end of each turn (used in AI minds)
aiTrapped(thisAIMap); //check if the ai is trapped at the end of each turn (used in AI minds)
thisAIMap['opponentInRange'] = targetInRange(thisAIMap, otherAIMap); //set if target is in range at the end of each turn (used in AI minds)
}
function removeDeadAI(AIMap)
{
threeworld.scene.remove(AIMap['mesh']);
AIMap['i'] = null;
AIMap['j'] = null;
}
function endRunProcedure()
{
updateStatus();
self.endCondition = true;
}
// --- enemy functions -----------------------------------
function drawEnemy() // given i and j, draw it
{
var x = translate ( enemyMap['i'] * squaresize );
var z = translate ( enemyMap['j'] * squaresize );
var y = 0;
enemyMap['mesh'].position.x = x;
enemyMap['mesh'].position.y = y;
enemyMap['mesh'].position.z = z;
threeworld.scene.add(enemyMap['mesh']);
threeworld.lookat.copy ( enemyMap['mesh'].position ); // if camera moving, look back at where the enemy is
}
function initLogicalEnemy()
{
// start in fixed location:
enemyMap['i'] = 11;
enemyMap['j'] = 21;
//get initial line of sight
totalLineOfSight(enemyMap);
}
function initThreeEnemy()
{
var shape = new THREE.BoxGeometry( squaresize, BOXHEIGHT, squaresize );
enemyMap['mesh'] = new THREE.Mesh( shape );
enemyMap['mesh'].material.color.setHex( BLANKCOLOR );
drawEnemy();
}
// --- agent functions -----------------------------------
function drawAgent() // given ai, aj, draw it
{
var x = translate ( agentMap['i'] * squaresize );
var z = translate ( agentMap['j'] * squaresize );
var y = 0;
agentMap['mesh'].position.x = x;
agentMap['mesh'].position.y = y;
agentMap['mesh'].position.z = z;
threeworld.scene.add(agentMap['mesh']);
threeworld.follow.copy ( agentMap['mesh'].position ); // follow vector = agent position (for camera following agent)
}
function initLogicalAgent()
{
// start in fixed location:
agentMap['i'] = 11;
agentMap['j'] = 1;
//get initial line of sight
totalLineOfSight(agentMap);
}
function initThreeAgent()
{
var shape = new THREE.BoxGeometry( squaresize, BOXHEIGHT, squaresize );
agentMap['mesh'] = new THREE.Mesh( shape );
agentMap['mesh'].material.color.setHex( BLANKCOLOR );
drawAgent();
}
function keyHandler(e)
// user control
// Note that this.takeAction(a) is constantly running at same time, redrawing the screen.
{
//agent key handling, movement is arrow keys
if (e.keyCode == 38) moveLogicalAI (agentMap, ACTION_UP); //up arrow key
if (e.keyCode == 37) moveLogicalAI (agentMap, ACTION_LEFT); //left arrow key
if (e.keyCode == 40) moveLogicalAI (agentMap, ACTION_DOWN); //down arrow key
if (e.keyCode == 39) moveLogicalAI (agentMap, ACTION_RIGHT); //right arrow key
if (e.keyCode == 45) attackTarget (agentMap, enemyMap); //insert key
//enemy key handling, movement is WASD keys
if (e.keyCode == 87) moveLogicalAI (enemyMap, ACTION_UP); //w key
if (e.keyCode == 65) moveLogicalAI (enemyMap, ACTION_LEFT); //a key
if (e.keyCode == 83) moveLogicalAI (enemyMap, ACTION_DOWN); //s key
if (e.keyCode == 68) moveLogicalAI (enemyMap, ACTION_RIGHT); //d key
if (e.keyCode == 32) attackTarget (enemyMap, agentMap); //space
}
// --- score: -----------------------------------
function calculateSingleScore(AIMap)
{
var currentScore = AIMap['health'] + AIMap['totalDamageDealt'];
if(aiHasDied)
{
if(AIMap['health'] > 0)
{
AIMap['score'] = currentScore + 100;
}
else
{
AIMap['score'] = currentScore - 50;
}
}
else
{
AIMap['score'] = currentScore;
}
}
function calculateScores()
{
calculateSingleScore(agentMap);
calculateSingleScore(enemyMap);
}
function aiTrapped(AIMap) // agent is blocked on all sides, run over
{
if( occupied (AIMap['i']-1,AIMap['j']) && occupied (AIMap['i']+1,AIMap['j']) && occupied (AIMap['i'],AIMap['j']+1) && occupied (AIMap['i'],AIMap['j']-1))
{
AIMap['trapped'] = true;
}
else
{
AIMap['trapped'] = false;
}
}
function updateStatus()
{
calculateScores();
//aligning text without css or bootstrap has made me a broken man, forgive me for what I have done.
var scores = 'Agent Score: ' + agentMap['score'] + ' Enemy Score: ' + ' ' + enemyMap['score'];
var aStatus = 'Agent Health: ' + agentMap['health'] + ' Agent Attack Damage: ' + agentMap['attackDamage'];
var eStatus = 'Enemy Health: ' + enemyMap['health'] + ' Enemy Attack Damage: ' + enemyMap['attackDamage'];
var aControls = 'Agent Controls: Move with arrow keys, Attack with insert key (NUM 0)';
var eControls = 'Enemy Controls: Move with WASD, Attack with spacebar ';
$("#user_span3").html(aStatus + ' ' + aControls + '<br>' );
$("#user_span4").html(eStatus + ' ' + eControls + '<br>' );
var stepsRemaining = MAXSTEPS - step;
$("#user_span5").html(scores + ' Steps Remaining: ' + stepsRemaining );
}
//--- state gathering for the AIs -----------------------------------------------------------------
function singleDirectionLineOfSight(AIMap, iModifier, jModifier)
{
//only 1 modifier should be != 0 in order to look either straight up, down, left or right, and this modifier
//should only be either 1 or -1 to avoid skipping squares
//a line of sight are the straight blocks in a direction plus 1 block on either side of this line
var curI = AIMap['i'] + iModifier; //add the modifier first to look at square this AI is not occupying
var curJ = AIMap['j'] + jModifier; //add the modifier first to look at square this AI is not occupying
while(!occupied(curI,curJ)) //note an assumption is made here that there is no gaps in the outter wall of the maze here
{
AIMap['lineOfSight'][curI][curJ] = GRID[curI][curJ];
if(iModifier != 0)//grab above and below blocks aswell
{
if(occupied(curI, curJ + 1) && GRID[curI][curJ + 1] != GRID_WALL)//check if the square is occupied by the opponent or a wall
{
AIMap['lineOfSight'][curI][curJ + 1] = GRID_OPPONENT_OCCUPIED; //this spot is blocked by the opponent
}
else
{
AIMap['lineOfSight'][curI][curJ + 1] = GRID[curI][curJ + 1];
}
if(occupied(curI, curJ - 1) && GRID[curI][curJ - 1] != GRID_WALL)//check if the square is occupied by the opponent or a wall
{
AIMap['lineOfSight'][curI][curJ - 1] = GRID_OPPONENT_OCCUPIED; //this spot is blocked by the opponent
}
else
{
AIMap['lineOfSight'][curI][curJ - 1] = GRID[curI][curJ - 1];
}
}
else //grab blocks to the left and right aswell
{
if(occupied(curI + 1, curJ) && GRID[curI + 1][curJ] != GRID_WALL)//check if the square is occupied by the opponent or a wall
{
AIMap['lineOfSight'][curI + 1][curJ] = GRID_OPPONENT_OCCUPIED; //this spot is blocked by the opponent
}
else
{
AIMap['lineOfSight'][curI + 1][curJ] = GRID[curI + 1][curJ];
}
if(occupied(curI - 1, curJ) && GRID[curI - 1][curJ] != GRID_WALL)//check if the square is occupied by the opponent or a wall
{
AIMap['lineOfSight'][curI - 1][curJ] = GRID_OPPONENT_OCCUPIED; //this spot is blocked by the opponent
}
else
{
AIMap['lineOfSight'][curI - 1][curJ] = GRID[curI - 1][curJ];
}
}
curI += iModifier;
curJ += jModifier;
}
//also add the square (and its neighbours) that is occupied to the line of sight
if(occupied(curI, curJ) && GRID[curI][curJ] != GRID_WALL)//check if the square is occupied by the opponent or a wall
{
AIMap['lineOfSight'][curI][curJ] = GRID_OPPONENT_OCCUPIED; //this spot is blocked by the opponent
}
else
{ //the sport is blocked by a wall, not the opponent
AIMap['lineOfSight'][curI][curJ] = GRID[curI][curJ];
}
//next grab the neighbouring two squares
if(iModifier != 0)//grab above and below blocks aswell
{
AIMap['lineOfSight'][curI][curJ + 1] = GRID[curI][curJ + 1];
AIMap['lineOfSight'][curI][curJ - 1] = GRID[curI][curJ - 1];
}
else //grab blocks to the left and right aswell
{
AIMap['lineOfSight'][curI + 1][curJ] = GRID[curI + 1][curJ];
AIMap['lineOfSight'][curI - 1][curJ] = GRID[curI - 1][curJ];
}
}
function totalLineOfSight(AIMap)
{
//get down line of sight
singleDirectionLineOfSight(AIMap, 0, 1);
//get up line of sight
singleDirectionLineOfSight(AIMap, 0, -1);
//get right line of sight
singleDirectionLineOfSight(AIMap, 1, 0);
//get left line of sight
singleDirectionLineOfSight(AIMap, -1, 0);
}
//--- 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;
// for all runs:
initGrid();
initLogicalMaze();
initLogicalAgent();
initLogicalEnemy();
// for graphical runs only:
if ( true )
{
if ( show3d )
{
BOXHEIGHT = squaresize;
threeworld.init3d ( startRadiusConst, maxRadiusConst, SKYCOLOR );
}
else
{
BOXHEIGHT = 1;
threeworld.init2d ( startRadiusConst, maxRadiusConst, SKYCOLOR );
}
initSkybox();
// Set up objects first:
initThreeMaze();
initThreeAgent();
initThreeEnemy();
// Then paint them with textures - asynchronous load of textures from files.
// The texture file loads return at some unknown future time in some unknown order.
// Because of the unknown order, it is probably best to make objects first and later paint them, rather than have the objects made when the file reads return.
// It is safe to paint objects in random order, but might not be safe to create objects in random order.
loadTextures();
document.onkeydown = keyHandler;
}
};
this.getState = function()
{
agentMap['opponentInRange'] = targetInRange(agentMap, enemyMap);
enemyMap['opponentInRange'] = targetInRange(enemyMap, agentMap);
var x = {
'agentMap': agentMap,
'enemyMap': enemyMap,
};
return ( x );
};
this.takeAction = function ( a )
{
step++;
if ( true )
updateStatus(); // show status line before moves
if(a != undefined && a['agentAction'] != undefined)
takeTurnLogicalAI(agentMap, enemyMap, a['agentAction']);
if(a != undefined && a['enemyAction'] != undefined)
takeTurnLogicalAI(enemyMap, agentMap, a['enemyAction']);
if ( true )
{
if(agentMap['health'] > 0) {
drawAgent();
}
else{
removeDeadAI(agentMap);
}
if(enemyMap['health'] > 0)
{
drawEnemy();
}
else
{
removeDeadAI(enemyMap);
}
updateStatus(); // show status line after moves
}
};
this.endRun = function()
{
if ( true )
{
$("#user_span6").html("<font color=red> <B> Run over. </B> </font> " );
}
};
this.getScore = function()
{
//Note that this function gets the highest of the two ai score's. This is the score that will be displayed
//on the world leaderboard, not the one ingame
if(agentMap['score'] > enemyMap['score'])
{
//console.log('end score = :' + agentMap['score']);
return agentMap['score'];
}
else
{
// console.log('end score = :' + enemyMap['score']);
return enemyMap['score'];
}
};
}
//---- end of World class -------------------------------------------------------