/**
* =======================================================================================================================
*
* The framework setup.
*
* =======================================================================================================================
*/
AB.mind.newRun = function () {
}
let initRoles = false;
AB.mind.getAction = function (world) {
let { agent, enemy } = world.roles;
if (!initRoles) {
roles.agent = new Agent(pos.agent, world, CONFIG.heuristic);
roles.enemy = new Enemy(pos.enemy, world);
agent = roles.agent;
enemy = roles.enemy;
initRoles = true;
}
// Enemy does not move on this step.
if (AB.step % CONFIG.roundCount !== 0) {
return {
agent: agent.getAction(world),
enemy: MOVE.stay
};
} else {
return {
agent: agent.getAction(world),
enemy: enemy.getAction(world)
};
}
};
/**
* =======================================================================================================================
*
* Roles
*
* =======================================================================================================================
*/
const ACTION_COUNT = 5;
const MOVE = {
stay: 0,
left: 1,
right: 2,
up: 3,
down: 4
};
class Role {
//! The current position.
_pos = { x: null, y: null };
//! Whether the role is trapped in a mud block.
_trapped = false;
//! Whether the previous move failed because of a wall.
_blocked = false;
//! The previous destination the role tries to move to.
_prevTryPos = null;
//! The move strategy chain.
_strategies = [new Random()];
//! The default action selector.
_selector = defaultSeletor(1);
constructor(pos, world) {
this._pos = pos;
}
/**
* Get the role's position.
*/
get pos() {
return this._pos;
}
/**
* Get the terrain of the role's position.
*/
get terrain() {
return terrain(this._pos);
}
/**
* Check whether the role is stuck.
*/
stuck(world) {
return occupied(dest(this._pos, MOVE.left), world) !== OCCUPIED.false
&& occupied(dest(this._pos, MOVE.right), world) !== OCCUPIED.false
&& occupied(dest(this._pos, MOVE.up), world) !== OCCUPIED.false
&& occupied(dest(this._pos, MOVE.down), world) !== OCCUPIED.false;
}
/**
* Move once.
*/
move(world) {
const action = this.getAction(world);
const newPos = dest(this._pos, action);
this.#tryToMove(newPos, world);
}
/**
* Get the action with the highest level of recommendation.
*
* @note Subclasses can implement their own choosing logic.
*/
getAction(world) {
let lvlMatrix = new Array(this._strategies.length);
for (let i = 0; i < this._strategies.length; i++) {
lvlMatrix[i] = this._strategies[i].getActionLvls(world);
}
return this._selector.highest(lvlMatrix);
}
/**
* Try to move to the destination.
*/
#tryToMove(pos, world) {
this._prevTryPos = pos;
this._blocked = false;
if (this.pos.x !== pos.x || this.pos.y !== pos.y) {
if (this._trapped) {
// Lost this turn because of being trapped.
this._trapped = false;
return;
}
const whyOccupied = occupied(pos, world);
if (whyOccupied !== OCCUPIED.false) {
// Blocked by a wall.
this._blocked = whyOccupied === OCCUPIED.wall;
return;
}
this._pos = pos;
// Trapped in mud.
this._trapped = terrain(pos) === TERRAIN.mud;
}
}
}
/**
* Get the destination after a move.
*/
function dest({ x, y }, action) {
switch (action) {
case MOVE.left: {
return { x: x - 1, y };
}
case MOVE.right: {
return { x: x + 1, y };
}
case MOVE.up: {
return { x, y: y + 1 };
}
case MOVE.down: {
return { x, y: y - 1 };
}
default: {
return { x, y };
}
}
}
/**
* Get the next action from the source to the destination.
*/
function actionTo(src, dest) {
const checkX = (src, dest) => {
if (src < dest) {
return MOVE.right;
} else if (src > dest) {
return MOVE.left;
} else {
return MOVE.stay;
}
};
const checkY = (src, dest) => {
if (src < dest) {
return MOVE.up;
} else if (src > dest) {
return MOVE.down;
} else {
return MOVE.stay;
}
};
const actX = checkX(src.x, dest.x);
const actY = checkY(src.y, dest.y);
if (actX === MOVE.stay) {
return actY;
} else if (actY === MOVE.stay) {
return actX;
} else {
return randomPick(actX, actY);
}
}
/**
* A role that can record the terrain of each position it moves to.
*/
class RoleWithTerrainMemo extends Role {
_terrains;
constructor(pos, world) {
super(pos, world);
this._terrains = new TerrainMemo(this, { height: CONFIG.gridSize, width: CONFIG.gridSize });
}
/**
* Update terrains before getting an action.
*/
getAction(world) {
this.#updateTerrain();
return super.getAction(world);
}
/**
* Get the terrain record.
*/
terrain(pos) {
if (this._terrains.valid(pos)) {
return this._terrains.get(pos);
} else {
return undefined;
}
}
/**
* Update the terrain record.
*/
#updateTerrain() {
const memo = this._terrains;
// Update the current position.
memo.set(this.pos, this.terrain);
if (this._blocked) {
// If blocked, then the previous destination is wall.
memo.set(this._prevTryPos, TERRAIN.wall);
}
}
}
class Agent extends RoleWithTerrainMemo {
#moveAway;
constructor(pos, world) {
super(pos, world);
this.#moveAway = new MoveAway(this, { height: CONFIG.gridSize, width: CONFIG.gridSize });
this._strategies = [new Random(), this.#moveAway, this._terrains];
this._selector = new ActionSelector([1, 0, 0])
}
getAction(world) {
this._selector.weights = [1, Math.random() * 0.4, Math.random() * 0.4];
return super.getAction(world);
}
}
class Enemy extends RoleWithTerrainMemo {
#aStar;
#moveClose;
constructor(pos, world, heuristic) {
super(pos, world);
this.#aStar = new AStar(this, this._terrains, heuristic);
this.#moveClose = new MoveClose(this, { height: CONFIG.gridSize, width: CONFIG.gridSize });
this._strategies = [new Random(), this.#aStar, this.#moveClose, this._terrains];
this._selector = new ActionSelector([0.1, 1, 0.2, 0.1])
}
/**
* Get the current A* path to the agent.
*/
get path() {
let path = this.#aStar.path;
if (!path) {
return null;
}
// Delete the previous beginning position.
const next = path[path.length - 2];
if (next.x === this.pos.x && next.y === this.pos.y) {
path.splice(path.length - 1, 1);
}
return path;
}
}
/**
* =======================================================================================================================
*
* Strategies.
*
* =======================================================================================================================
*/
const MAX_ACT_LVL = 10;
/**
* The interface of move strategy.
*/
class Strategy {
/**
* Get an array containing the level of recommendation for every action.
*
* @pure
* @note Subclasses should implement their move logic.
* @warning The level of recommendation is between 0 and 10.
*/
getActionLvls(world) {
console.assert(false);
}
}
function emptyActionLvls() {
return new Array(ACTION_COUNT).fill(0);
}
/**
* Get the action with the highest level of recommendation according to the specific weights.
*/
class ActionSelector {
#weights;
constructor(weights) {
this.#weights = weights;
}
set weights(val) {
this.#weights = val;
}
highest(lvlMatrix) {
let sum = emptyActionLvls();
for (let w = 0; w < this.#weights.length; w++) {
mergeActionLvls(sum, lvlMatrix[w], this.#weights[w]);
}
// If two or more actions have the same level, get a random one.
let best = 0;
let possible = [best];
for (let i = 0; i < ACTION_COUNT; i++) {
if (sum[i] === sum[best]) {
possible.push(i);
} else if (sum[i] > sum[best]) {
possible = [i];
best = i;
}
}
return possible[randomInt(0, possible.length - 1)];
}
}
/**
* Get a default action seletor with all weights are 1.
*/
function defaultSeletor(weightCount) {
const weights = new Array(weightCount).fill(1);
return new ActionSelector(weights);
}
/**
* Merge two action recommendation arrays.
*/
function mergeActionLvls(sum, newlvls, weight) {
for (let i = 0; i < ACTION_COUNT; i++) {
sum[i] += newlvls[i] * weight;
}
}
/**
* Get an action randomly.
*/
class Random {
getActionLvls(world) {
let lvls = new Array(ACTION_COUNT).fill(MAX_ACT_LVL);
lvls[MOVE.stay] = 0;
return lvls;
}
}
/**
* Get an action to move away to the target.
*/
class MoveAway {
#self;
#target = null;
constructor(role) {
this.#self = role;
}
getActionLvls(world) {
if (!this.#target) {
this.#target = opponent(this.#self, world);
}
let lvls = emptyActionLvls();
const target = this.#target.pos;
const { x, y } = this.#self.pos;
if (target.x <= x) {
lvls[MOVE.right] = MAX_ACT_LVL;
} else if (target.x >= x) {
lvls[MOVE.left] = MAX_ACT_LVL;
}
if (target.y <= y) {
lvls[MOVE.up] = MAX_ACT_LVL;
} else if (target.y >= y) {
lvls[MOVE.down] = MAX_ACT_LVL;
}
return lvls;
}
}
/**
* Get an action to move closer to the target.
*/
class MoveClose {
#self;
#target = null;
constructor(role) {
this.#self = role;
}
getActionLvls(world) {
if (!this.#target) {
this.#target = opponent(this.#self, world);
}
let lvls = emptyActionLvls();
const target = this.#target.pos;
const { x, y } = this.#self.pos;
if (target.x < x) {
lvls[MOVE.left] = MAX_ACT_LVL;
} else if (target.x > x) {
lvls[MOVE.right] = MAX_ACT_LVL;
}
if (target.y < y) {
lvls[MOVE.down] = MAX_ACT_LVL;
} else if (target.y > y) {
lvls[MOVE.up] = MAX_ACT_LVL;
}
return lvls;
}
}
class TerrainMemo {
#self;
#memo;
#size;
static #RANGE = 5;
constructor(role, { width, height }) {
this.#size = { width, height };
this.#self = role;
// The tarrains of all positions are unknown.
this.#memo = new Array(width);
for (let x = 0; x < width; x++) {
this.#memo[x] = new Array(height);
for (let y = 0; y < height; y++) {
this.#memo[x][y] = TERRAIN.unknown
}
}
}
getActionLvls(world) {
let lvls = emptyActionLvls();
for (let i = 0; i < ACTION_COUNT; i++) {
const newPos = dest(this.#self.pos, i);
if (this.valid(newPos) && !this.wall(newPos)) {
lvls[i] = MAX_ACT_LVL - this.#wallDensity(i) * 0.5;
}
}
lvls[MOVE.stay] = 0;
return lvls;
}
set({ x, y }, type) {
this.#memo[x][y] = type;
}
get({ x, y }) {
return this.#memo[x][y];
}
wall({ x, y }) {
return this.#memo[x][y] === TERRAIN.wall;
}
valid({ x, y }) {
const validRow = 0 <= y && y < this.#size.height;
const validCol = 0 <= x && x < this.#size.width;
return validRow && validCol;
}
get width() {
return this.#size.width;
}
get height() {
return this.#size.height;
}
get size() {
return this.#size;
}
#wallDensity(action) {
const range = TerrainMemo.#RANGE;
let x = 0, y = 0;
let endX = 0, endY = 0;
const pos = this.#self.pos;
switch (action) {
case MOVE.left: {
x = pos.x - range > 0 ? pos.x - range : 0;
endX = pos.x;
y = pos.y - range > 0 ? pos.y - range : 0;
endY = pos.y + range < this.height ? pos.y + range : this.height;
break;
}
case MOVE.right: {
x = pos.x;
endX = pos.x + range < this.width ? pos.x + range : this.width;
y = pos.y - range > 0 ? pos.y - range : 0;
endY = pos.y + range < this.height ? pos.y + range : this.height;
break;
}
case MOVE.up: {
x = pos.x - range > 0 ? pos.x - range : 0;
endX = pos.x + range < this.width ? pos.x + range : this.width;
y = pos.y;
endY = pos.y + range < this.height ? pos.y + range : this.height;
break;
}
case MOVE.down: {
x = pos.x - range > 0 ? pos.x - range : 0;
endX = pos.x + range < this.width ? pos.x + range : this.width;
y = pos.y - range > 0 ? pos.y - range : 0;
endY = pos.y;
break;
}
default: {
return MAX_ACT_LVL;
}
}
let total = 1, wall = 1;
if (x === 0 || endX === this.width
|| y === 0 || endY === this.height) {
total += range * 2;
wall += range * 2;
}
for (; x !== endX; x++) {
for (; y !== endY; y++) {
total++;
if (this.wall({ x, y })) {
wall++;
}
}
}
return Math.round((wall / total) * MAX_ACT_LVL);
}
}
/**
* =======================================================================================================================
*
* A*
*
* =======================================================================================================================
*/
class Spot {
#pos;
#neighbors = [];
#terrains;
previous = null;
#g = 0;
#h = 0;
#f = 0;
constructor(pos, terrains) {
this.#pos = pos;
this.#terrains = terrains;
}
addNeighbors(grid) {
const memo = this.#terrains;
const neighbors = this.#neighbors;
const { x, y } = this.#pos;
if (memo.valid({ x: x, y: y - 1 })) {
neighbors.push(grid[x][y - 1]);
}
if (memo.valid({ x: x, y: y + 1 })) {
neighbors.push(grid[x][y + 1]);
}
if (memo.valid({ x: x - 1, y: y })) {
neighbors.push(grid[x - 1][y]);
}
if (memo.valid({ x: x + 1, y: y })) {
neighbors.push(grid[x + 1][y]);
}
}
get neighbors() {
const memo = this.#terrains;
const neighbors = this.#neighbors;
// Delete the neighbor which has been known as a wall.
for (let i = 0; i < neighbors.length; i++) {
if (memo.wall(neighbors[i])) {
neighbors.splice(i, 1);
i--;
}
}
return neighbors;
}
get pos() {
return this.#pos;
}
get y() {
return this.#pos.y;
}
get x() {
return this.#pos.x;
}
get g() {
return this.#g;
}
set g(val) {
this.#g = val;
this.#f = this.g + this.h;
}
get h() {
return this.#h;
}
set h(val) {
this.#h = val;
this.#f = this.g + this.h;
}
get f() {
return this.#f;
}
clear() {
this.#g = 0;
this.#h = 0;
this.#f = 0;
this.previous = null;
}
}
/**
* A* pathfinding.
*/
class AStar {
#self;
#target;
#heuristic = (src, dest) => 0;
#terrains;
#pathGrid;
#path;
static #line;
static #points;
constructor(role, terrains, heuristic) {
this.#self = role;
if (heuristic) {
this.#heuristic = heuristic;
}
this.#terrains = terrains;
this.#initPathGrid(terrains);
const material = new THREE.LineBasicMaterial( { color: 0xff0000 } );
const geometry = new THREE.BufferGeometry();
AStar.#points = new Float32Array(CONFIG.gridSize * CONFIG.gridSize * 3 )
geometry.addAttribute( 'position', new THREE.BufferAttribute(AStar.#points, 3));
geometry.setDrawRange( 0, 0 );
AStar.#line = new THREE.Line( geometry, material );
ABWorld.scene.add( AStar.#line );
}
getActionLvls(world) {
if (!this.#target) {
this.#target = opponent(this.#self, world);
}
let lvls = emptyActionLvls();
this.#clearPathGrid();
const src = this.#self.pos;
const dest = this.#target.pos;
let openSet = [];
let closedSet = [];
openSet.push(this.#pathGrid[src.x][src.y]);
while (openSet.length > 0) {
const pos = AStar.#nextPos(openSet);
if (AStar.#isEnd(pos, dest)) {
// Find a valid path.
this.#path = AStar.#trackPath(pos);
const next = this.#path[this.#path.length - 2];
lvls[actionTo(src, next)] = MAX_ACT_LVL;
break;
}
closedSet.push(pos);
// The neighbors having unknown terrains will be regarded as blank blocks.
const neighbors = pos.neighbors;
for (let i = 0; i < neighbors.length; i++) {
const neighbor = neighbors[i];
if (!closedSet.includes(neighbor)) {
// Consider the move cost of the neighbor when calculating `g`.
const newG = pos.g + cost(this.#terrains.get(neighbor.pos));
+ this.#heuristic(neighbor, pos);
let newPath = false;
if (openSet.includes(neighbor)) {
if (newG < neighbor.g) {
neighbor.g = newG;
newPath = true;
}
} else {
neighbor.g = newG;
newPath = true;
openSet.push(neighbor);
}
if (newPath) {
neighbor.h = this.#heuristic(neighbor, dest);
neighbor.previous = pos;
}
}
}
}
return lvls;
}
get path() {
return this.#path;
}
/**
* Create a new terrain record.
*/
#initPathGrid({ width, height }) {
// The tarrains of all positions are unknown.
this.#pathGrid = new Array(width);
for (let x = 0; x < width; x++) {
this.#pathGrid[x] = new Array(height);
for (let y = 0; y < height; y++) {
this.#pathGrid[x][y] = new Spot({ x, y }, this.#terrains);
}
}
for (let x = 0; x < width; x++) {
for (let y = 0; y < height; y++) {
this.#pathGrid[x][y].addNeighbors(this.#pathGrid);
}
}
}
/**
* Clear the previous search data.
*/
#clearPathGrid() {
this.#path = null;
for (let x = 0; x < this.#pathGrid.length; x++) {
for (let y = 0; y < this.#pathGrid[x].length; y++) {
if (this.#pathGrid[x][y]) {
this.#pathGrid[x][y].clear();
}
}
}
}
static #nextPos(openSet) {
let next = 0;
for (let i = 1; i < openSet.length; i++) {
if (openSet[i].f < openSet[next].f) {
next = i;
}
}
const pos = openSet[next];
openSet.splice(next, 1);
return pos;
}
/**
* Check if a position is the destination.
*/
static #isEnd(pos, dest) {
const { x: endX, y: endY } = dest;
return pos.x === endX && pos.y === endY;
}
/**
* Get the entire path ending with a position.
*/
static #trackPath(spot) {
const points = AStar.#line.geometry.attributes.position.array;
let i = 0;
let count = 0;
let path = [];
let curr = spot;
path.push(curr);
while (curr.previous) {
path.push(curr.previous);
points[i++] = (curr.previous.x * CONFIG.squareSize) - (maxPos / 2);
points[i++] = 0
points[i++] = (curr.previous.y * CONFIG.squareSize) - (maxPos / 2);
curr = curr.previous;
count++;
}
AStar.#line.geometry.attributes.position.needsUpdate = true;
AStar.#line.geometry.setDrawRange(0, count);
ABWorld.render();
return path;
}
}