/**
* Basic configurations.
*/
const CONFIG = {
//! Whether to display images in 3D mode.
show3d: true,
//! Number of squares along side of world.
gridSize: 50, //50
//! Size of square in pixels.
squareSize: 100,
//! The probability of generating each kind of blocks.
threshold: {
wall: 0.3,
net: 0.2 // 0.2
},
//! Movement costs of different terrains.
cost: {
blank: 1,
net: 15
},
//! The number of rounds between the enemy's twice moves.
roundCount: 2,
//! The estimate function.
heuristic: (src, dest) => {
return Math.abs(src.x - dest.x) + Math.abs(src.y - dest.y);
},
//! Use a slower frame rate to see how it is working.
frameRate: null
};
/**
* The positions of agent and enemy.
*/
let pos = {
agent: { x: null, y: null },
enemy: { x: null, y: null }
};
/**
* Used to control the movement of agent and enemy.
*
* @note Be Initialized in `AB.mind.newRun`
*/
let roles = {
agent: null,
enemy: null
};
let steps = {
bad: 0,
good: 0
};
/**
* =======================================================================================================================
*
* Maze.
*
* =======================================================================================================================
*/
/**
* Types of terrains.
*/
const TERRAIN = {
unknow: 0,
blank: 1,
wall: 2,
net: 3
};
function cost(terrain) {
switch (terrain) {
case TERRAIN.wall: {
return Infinity;
}
case TERRAIN.net: {
return CONFIG.cost.net;
}
default: {
return CONFIG.cost.blank;
}
}
}
//! Can query GRID about whether squares are occupied, will in fact be initialised as a 2D array.
let grid = new Array(CONFIG.gridSize);
function wall({ x, y }) {
return grid[x][y] === TERRAIN.wall;
}
function terrain({ x, y }) {
return grid[x][y];
}
function valid({ x, y }) {
const validRow = 0 <= y && y < CONFIG.gridSize;
const validCol = 0 <= x && x < CONFIG.gridSize;
return validRow && validCol;
}
/**
* =======================================================================================================================
*
* Initialization.
*
* =======================================================================================================================
*/
/**
* The number of wall blocks.
*/
const wallBoxes = Math.trunc((CONFIG.gridSize * CONFIG.gridSize) * CONFIG.threshold.wall);
/**
* The number of spider net blocks.
*/
const netBoxes = Math.trunc((CONFIG.gridSize * CONFIG.gridSize) * CONFIG.threshold.net);
//! Length of one side in pixels .
const maxPos = CONFIG.gridSize * CONFIG.squareSize;
//! Distance from centre to start the camera at.
const startRadius = maxPos * 0.8;
//! Maximum distance from camera we will render things.
const maxRadius = maxPos * 10;
//! 3D or 2D box height.
let boxHeight;
let textures = {
wall: null,
net: null,
fog: null,
agent: null,
enemy: null
};
/**
* Used to display the agent and enemy.
*/
let displayers = {
agent: null,
enemy: null
};
// Change ABWorld defaults.
ABHandler.MAXCAMERAPOS = maxRadius;
//! Ground exists at altitude zero.
ABHandler.GROUNDZERO = true;
//! Asynchronous file loads - call initScene() when all finished.
function loadResources() {
const wall = '/uploads/czs108/stone.png';
const agent = '/uploads/czs108/pig.png';
const enemy = '/uploads/czs108/zombie.png';
const net = '/uploads/czs108/net.png';
const loader1 = new THREE.TextureLoader();
const loader2 = new THREE.TextureLoader();
const loader3 = new THREE.TextureLoader();
const loader4 = new THREE.TextureLoader();
loader1.load(wall, function (texture) {
texture.minFilter = THREE.LinearFilter;
textures.wall = texture;
if (asynchFinished()) {
initScene();
}
});
loader2.load(agent, function (texture) {
texture.minFilter = THREE.LinearFilter;
textures.agent = texture;
if (asynchFinished()) {
initScene();
}
});
loader3.load(enemy, function (texture) {
texture.minFilter = THREE.LinearFilter;
textures.enemy = texture;
if (asynchFinished()) {
initScene();
}
});
loader4.load(net, function (texture) {
texture.minFilter = THREE.LinearFilter;
textures.net = texture;
if (asynchFinished()) {
initScene();
}
});
}
function asynchFinished() {
if (textures.wall && textures.agent && textures.enemy && textures.net) {
return true;
} else {
return false;
}
}
function translate({ x, y }) {
let v = new THREE.Vector3();
v.y = 0;
v.x = (x * CONFIG.squareSize) - (maxPos / 2);
v.z = (y * CONFIG.squareSize) - (maxPos / 2);
return v;
}
function initScene() {
// set up maze
for (let x = 0; x < CONFIG.gridSize; x++) {
grid[x] = new Array(CONFIG.gridSize);
for (let y = 0; y < CONFIG.gridSize; y++) {
if ((x == 0) || (x == CONFIG.gridSize - 1) || (y == 0) || (y == CONFIG.gridSize - 1)) {
grid[x][y] = TERRAIN.wall;
const shape = new THREE.BoxGeometry(CONFIG.squareSize, boxHeight, CONFIG.squareSize);
let cube = new THREE.Mesh(shape);
cube.material = new THREE.MeshBasicMaterial({ map: textures.wall });
cube.position.copy(translate({ x, y }));
ABWorld.scene.add(cube);
} else {
grid[x][y] = TERRAIN.blank;
}
}
}
// set up walls
for (let c = 1; c <= wallBoxes; c++) {
const x = AB.randomIntAtoB(1, CONFIG.gridSize - 2);
const y = AB.randomIntAtoB(1, CONFIG.gridSize - 2);
grid[x][y] = TERRAIN.wall;
const shape = new THREE.BoxGeometry(CONFIG.squareSize, boxHeight, CONFIG.squareSize);
let cube = new THREE.Mesh(shape);
cube.material = new THREE.MeshBasicMaterial({ map: textures.wall });
cube.position.copy(translate({ x, y }));
ABWorld.scene.add(cube);
}
// set up net
for (let c = 1; c <= netBoxes; c++) {
const x = AB.randomIntAtoB(1, CONFIG.gridSize - 2);
const y = AB.randomIntAtoB(1, CONFIG.gridSize - 2);
if (terrain({ x, y }) !== TERRAIN.blank) {
continue;
}
grid[x][y] = TERRAIN.net;
const shape = new THREE.BoxGeometry(CONFIG.squareSize, boxHeight, CONFIG.squareSize);
let cube = new THREE.Mesh(shape);
cube.material = new THREE.MeshBasicMaterial({ map: textures.net });
cube.position.copy(translate({ x, y }));
ABWorld.scene.add(cube);
}
// set up enemy
// start in random location
let x, y;
do {
x = AB.randomIntAtoB(1, CONFIG.gridSize - 2);
y = AB.randomIntAtoB(1, CONFIG.gridSize - 2);
} while (occupied({ x, y }));
pos.enemy = { x, y }
let shape = new THREE.BoxGeometry(CONFIG.squareSize, boxHeight, CONFIG.squareSize);
displayers.enemy = new THREE.Mesh(shape);
displayers.enemy.material = new THREE.MeshBasicMaterial({ map: textures.enemy });
ABWorld.scene.add(displayers.enemy);
drawEnemy();
// set up agent
// start in random location
do {
x = AB.randomIntAtoB(1, CONFIG.gridSize - 2);
y = AB.randomIntAtoB(1, CONFIG.gridSize - 2);
} while (occupied({ x, y }));
pos.agent = { x, y }
shape = new THREE.BoxGeometry(CONFIG.squareSize, boxHeight, CONFIG.squareSize);
displayers.agent = new THREE.Mesh(shape);
displayers.agent.material = new THREE.MeshBasicMaterial({ map: textures.agent });
ABWorld.scene.add(displayers.agent);
drawAgent();
// finally skybox
// setting up skybox is simple
// just pass it array of 6 URLs and it does the asych load
const skyBoxes = [
"/uploads/czs108/win10paper.png",
"/uploads/czs108/win10paper.png",
"/uploads/czs108/win10paper.png",
"/uploads/czs108/win10paper.png",
"/uploads/czs108/win10paper.png",
"/uploads/czs108/win10paper.png"
];
ABWorld.scene.background = new THREE.CubeTextureLoader().load(skyBoxes, function () {
ABWorld.render();
AB.removeLoading();
AB.runReady = true;
});
}
function drawEnemy() {
const enemy = displayers.enemy;
enemy.position.copy(translate(pos.enemy));
ABWorld.lookat.copy(enemy.position);
}
function drawAgent() {
const agent = displayers.agent;
agent.position.copy(translate(pos.agent));
ABWorld.follow.copy(agent.position);
}
/**
* =======================================================================================================================
*
* Keyboard input handling.
*
* =======================================================================================================================
*/
const OURKEYS = [37, 38, 39, 40];
function ourKeys(event) {
return (OURKEYS.includes(event.keyCode));
}
function keyHandler(event) {
if (!AB.runReady) {
return true;
}
if (!ourKeys(event)) {
return true;
}
if (event.keyCode == 37) {
roles.agent.move(MOVE.left);
} else if (event.keyCode == 38) {
roles.agent.move(MOVE.down);
} else if (event.keyCode == 39) {
roles.agent.move(MOVE.right);
} else if (event.keyCode == 40) {
roles.agent.move(MOVE.up);
}
event.stopPropagation();
event.preventDefault();
return false;
}
/**
* =======================================================================================================================
*
* Score.
*
* =======================================================================================================================
*/
function badStep() {
const { x: ax, y: ay } = pos.agent;
const { x: ex, y: ey } = pos.enemy;
if ((Math.abs(ex - ax) < 2) && (Math.abs(ey - ay) < 2)) {
return true;
} else {
return false;
}
}
function updateStatusBefore(action) {
const x = AB.world.getState();
AB.msg(" Step: " + AB.step + " x = (" + x.toString() + ") a = (" + action + ") ");
}
function updateStatusAfter() {
const y = AB.world.getState();
const score = (steps.good / AB.step) * 100;
AB.msg(" y = (" + y.toString() + ") <br>" +
" Bad steps: " + steps.bad +
" Good steps: " + steps.good +
" Score: " + score.toFixed(2) + "% ", 2);
}
/**
* The result of occupation check.
*/
const OCCUPIED = {
//! No occupation.
false: 0,
//! The position is invalid.
invalid: 1,
//! The position is a wall.
wall: 2,
//! The position has been occupied by a role.
role: 3
};
/**
* If this square is occupied.
*/
function occupied({ x, y }) {
if (!valid({ x, y })) {
return OCCUPIED.invalid;
} else if (wall({ x, y })) {
return OCCUPIED.wall;
}
const { agent, enemy } = pos;
if (x === agent.x && y === agent.y
|| x === enemy.x && y === enemy.y) {
return OCCUPIED.role;
}
return OCCUPIED.false;
}
function opponent(role, world) {
if (role === world.roles.agent) {
return world.roles.enemy;
} else if (role === world.roles.enemy) {
return world.roles.agent;
} else {
return null;
}
}
function randomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function randomPick(a, b) {
return [a, b][randomInt(0, 1)]
}
/**
* =======================================================================================================================
*
* The framework setup.
*
* =======================================================================================================================
*/
AB.world.newRun = function () {
//! @warning: It's a number rather than a string.
const skyColor = 0xDDFFDD;
AB.loadingScreen();
AB.runReady = false;
steps.bad = 0;
steps.good = 0;
if (CONFIG.show3d) {
boxHeight = CONFIG.squareSize;
ABWorld.init3d(startRadius, maxRadius, skyColor);
} else {
boxHeight = 1;
ABWorld.init2d(startRadius, maxRadius, skyColor);
}
loadResources();
document.onkeydown = keyHandler;
};
/**
* @return Include the agent and enemy.
*/
AB.world.getState = function () {
return { grid, roles };
};
/**
* @param actions Include agent's action and enemy's action.
*/
AB.world.takeAction = function (actions) {
updateStatusBefore(actions.agent);
// Move agent and enemy.
roles.agent.move(actions.agent);
pos.agent = roles.agent.pos;
if ((AB.step % CONFIG.roundCount) == 0) {
roles.enemy.move(actions.enemy);
pos.enemy = roles.enemy.pos;
}
if (badStep()) {
steps.bad++;
} else {
steps.good++;
}
drawAgent();
drawEnemy();
updateStatusAfter();
if (roles.agent.stuck()) {
AB.abortRun = true;
steps.good = 0;
}
};
AB.world.endRun = function () {
if (AB.abortRun) {
AB.msg(" <br> <font color=red> <B> Agent trapped. Final score zero. </B> </font> ", 3);
} else {
AB.msg(" <br> <font color=green> <B> Run over. </B> </font> ", 3);
}
};
AB.world.getScore = function () {
const s = (steps.good / AB.maxSteps) * 100
const x = Math.round(s * 100);
return (x / 100);
};