Code viewer for World: A* Chase Mind
/**
 * 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 + " &nbsp; x = (" + x.toString() + ") &nbsp; a = (" + action + ") ");
}

function updateStatusAfter() {
    const y = AB.world.getState();
    const score = (steps.good / AB.step) * 100;
    AB.msg(" &nbsp; y = (" + y.toString() + ") <br>" +
        " Bad steps: " + steps.bad +
        " &nbsp; Good steps: " + steps.good +
        " &nbsp; 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);
};