Source: terrain-analysis.js

/**
 * @class
 */
var API3 = function(m)
{

/**
 * TerrainAnalysis, inheriting from the Map Component.
 *
 * This creates a suitable passability map.
 * This is part of the Shared Script, and thus should only be used for things that are non-player specific.
 * This.map is a map of the world, where particular stuffs are pointed with a value
 * For example, impassable land is 0, water is 200, areas near tree (ie forest grounds) are 41…
 * This is intended for use with 8 bit maps for reduced memory usage.
 * Upgraded from QuantumState's original TerrainAnalysis for qBot.
 */

m.TerrainAnalysis = function()
{
};

m.copyPrototype(m.TerrainAnalysis, m.Map);

m.TerrainAnalysis.prototype.IMPASSABLE = 0;
/**
* non-passable by land units
*/
m.TerrainAnalysis.prototype.DEEP_WATER = 200;
/**
* passable by land units and water units
*/
m.TerrainAnalysis.prototype.SHALLOW_WATER = 201;
/**
* passable by land units
*/
m.TerrainAnalysis.prototype.LAND = 255;

m.TerrainAnalysis.prototype.init = function(sharedScript, rawState)
{
	let passabilityMap = rawState.passabilityMap;
	this.width = passabilityMap.width;
	this.height = passabilityMap.height;
	this.cellSize = passabilityMap.cellSize;

	let obstructionMaskLand = rawState.passabilityClasses["default-terrain-only"];
	let obstructionMaskWater = rawState.passabilityClasses["ship-terrain-only"];

	let obstructionTiles = new Uint8Array(passabilityMap.data.length);


	for (let i = 0; i < passabilityMap.data.length; ++i)
	{
		obstructionTiles[i] = (passabilityMap.data[i] & obstructionMaskLand) ? this.IMPASSABLE : this.LAND;

		if (!(passabilityMap.data[i] & obstructionMaskWater) && obstructionTiles[i] === this.IMPASSABLE)
			obstructionTiles[i] = this.DEEP_WATER;
		else if (!(passabilityMap.data[i] & obstructionMaskWater) && obstructionTiles[i] === this.LAND)
			obstructionTiles[i] = this.SHALLOW_WATER;
	}

	this.Map(rawState, "passability", obstructionTiles);
};

/**
 * Accessibility inherits from TerrainAnalysis
 *
 * This can easily and efficiently determine if any two points are connected.
 * it can also determine if any point is "probably" reachable, assuming the unit can get close enough
 * for optimizations it's called after the TerrainAnalyser has finished initializing his map
 * so this can use the land regions already.
 */
m.Accessibility = function()
{
};

m.copyPrototype(m.Accessibility, m.TerrainAnalysis);

m.Accessibility.prototype.init = function(rawState, terrainAnalyser)
{
	this.Map(rawState, "passability", terrainAnalyser.map);
	this.landPassMap = new Uint16Array(terrainAnalyser.length);
	this.navalPassMap = new Uint16Array(terrainAnalyser.length);

	this.maxRegions = 65535;
	this.regionSize = [];
	this.regionType = []; // "inaccessible", "land" or "water";
	// ID of the region associated with an array of region IDs.
	this.regionLinks = [];

	// initialized to 0, it's more optimized to start at 1 (I'm checking that if it's not 0, then it's already aprt of a region, don't touch);
	// However I actually store all unpassable as region 1 (because if I don't, on some maps the toal nb of region is over 256, and it crashes as the mapis 8bit.)
	// So start at 2.
	this.regionID = 2;

	for (let i = 0; i < this.landPassMap.length; ++i)
	{
		if (this.map[i] !== this.IMPASSABLE)
		{	// any non-painted, non-inacessible area.
			if (this.landPassMap[i] == 0 && this.floodFill(i, this.regionID, false))
				this.regionType[this.regionID++] = "land";
			if (this.navalPassMap[i] == 0 && this.floodFill(i, this.regionID, true))
				this.regionType[this.regionID++] = "water";
		}
		else if (this.landPassMap[i] == 0)
		{	// any non-painted, inacessible area.
			this.floodFill(i, 1, false);
			this.floodFill(i, 1, true);
		}
	}

	// calculating region links. Regions only touching diagonaly are not linked.
	// since we're checking all of them, we'll check from the top left to the bottom right
	let w = this.width;
	for (let x = 0; x < this.width-1; ++x)
	{
		for (let y = 0; y < this.height-1; ++y)
		{
			// checking right.
			let thisLID = this.landPassMap[x+y*w];
			let thisNID = this.navalPassMap[x+y*w];
			let rightLID = this.landPassMap[x+1+y*w];
			let rightNID = this.navalPassMap[x+1+y*w];
			let bottomLID = this.landPassMap[x+y*w+w];
			let bottomNID = this.navalPassMap[x+y*w+w];
			if (thisLID > 1)
			{
				if (rightNID > 1)
					if (this.regionLinks[thisLID].indexOf(rightNID) === -1)
						this.regionLinks[thisLID].push(rightNID);
				if (bottomNID > 1)
					if (this.regionLinks[thisLID].indexOf(bottomNID) === -1)
						this.regionLinks[thisLID].push(bottomNID);
			}
			if (thisNID > 1)
			{
				if (rightLID > 1)
					if (this.regionLinks[thisNID].indexOf(rightLID) === -1)
						this.regionLinks[thisNID].push(rightLID);
				if (bottomLID > 1)
					if (this.regionLinks[thisNID].indexOf(bottomLID) === -1)
						this.regionLinks[thisNID].push(bottomLID);
				if (thisLID > 1)
					if (this.regionLinks[thisNID].indexOf(thisLID) === -1)
						this.regionLinks[thisNID].push(thisLID);
			}
		}
	}

	// Engine.DumpImage("LandPassMap.png", this.landPassMap, this.width, this.height, 255);
	// Engine.DumpImage("NavalPassMap.png", this.navalPassMap, this.width, this.height, 255);
};

m.Accessibility.prototype.getAccessValue = function(position, onWater)
{
	let gamePos = this.gamePosToMapPos(position);
	if (onWater)
		return this.navalPassMap[gamePos[0] + this.width*gamePos[1]];
	let ret = this.landPassMap[gamePos[0] + this.width*gamePos[1]];
	if (ret === 1)
	{
		// quick spiral search.
		let indx = [ [-1, -1], [-1, 0], [-1, 1], [0, 1], [1, 1], [1, 0], [1, -1], [0, -1]];
		for (let i of indx)
		{
			let id0 = gamePos[0] + i[0];
			let id1 = gamePos[1] + i[1];
			if (id0 < 0 || id0 >= this.width || id1 < 0 || id1 >= this.width)
				continue;
			ret = this.landPassMap[id0 + this.width*id1];
			if (ret !== 1)
				return ret;
		}
	}
	return ret;
};

m.Accessibility.prototype.getTrajectTo = function(start, end)
{
	let pstart = this.gamePosToMapPos(start);
	let istart = pstart[0] + pstart[1]*this.width;
	let pend = this.gamePosToMapPos(end);
	let iend = pend[0] + pend[1]*this.width;

	let onLand = true;
	if (this.landPassMap[istart] <= 1 && this.navalPassMap[istart] > 1)
		onLand = false;
	if (this.landPassMap[istart] <= 1 && this.navalPassMap[istart] <= 1)
		return false;

	let endRegion = this.landPassMap[iend];
	if (endRegion <= 1 && this.navalPassMap[iend] > 1)
		endRegion = this.navalPassMap[iend];
	else if (endRegion <= 1)
		return false;

	let startRegion = onLand ? this.landPassMap[istart] : this.navalPassMap[istart];
	return this.getTrajectToIndex(startRegion, endRegion);
};

/**
 * Return a "path" of accessibility indexes from one point to another, including the start and the end indexes
 * this can tell you what sea zone you need to have a dock on, for example.
 * assumes a land unit unless start point is over deep water.
 */
m.Accessibility.prototype.getTrajectToIndex = function(istart, iend)
{
	if (istart === iend)
		return [istart];

	let trajects = new Set();
	let explored = new Set();
	trajects.add([istart]);
	explored.add(istart);
	while (trajects.size)
	{
		for (let traj of trajects)
		{
			let ilast = traj[traj.length-1];
			for (let inew of this.regionLinks[ilast])
			{
				if (inew === iend)
					return traj.concat(iend);
				if (explored.has(inew))
					continue;
				trajects.add(traj.concat(inew));
				explored.add(inew);
			}
			trajects.delete(traj);
		}
	}
	return undefined;
};

m.Accessibility.prototype.getRegionSize = function(position, onWater)
{
	let pos = this.gamePosToMapPos(position);
	let index = pos[0] + pos[1]*this.width;
	let ID = onWater === true ? this.navalPassMap[index] : this.landPassMap[index];
	if (this.regionSize[ID] === undefined)
		return 0;
	return this.regionSize[ID];
};

m.Accessibility.prototype.getRegionSizei = function(index, onWater)
{
	if (this.regionSize[this.landPassMap[index]] === undefined && (!onWater || this.regionSize[this.navalPassMap[index]] === undefined))
		return 0;
	if (onWater && this.regionSize[this.navalPassMap[index]] > this.regionSize[this.landPassMap[index]])
		return this.regionSize[this.navalPassMap[index]];
	return this.regionSize[this.landPassMap[index]];
};

/** Implementation of a fast flood fill. Reasonably good performances for JS. */
m.Accessibility.prototype.floodFill = function(startIndex, value, onWater)
{
	if (value > this.maxRegions)
	{
		error("AI accessibility map: too many regions.");
		this.landPassMap[startIndex] = 1;
		this.navalPassMap[startIndex] = 1;
		return false;
	}

	if (!onWater && this.landPassMap[startIndex] != 0 || onWater && this.navalPassMap[startIndex] != 0)
		return false;	// already painted.

	let floodFor = "land";
	if (this.map[startIndex] === this.IMPASSABLE)
	{
		this.landPassMap[startIndex] = 1;
		this.navalPassMap[startIndex] = 1;
		return false;
	}

	if (onWater === true)
	{
		if (this.map[startIndex] !== this.DEEP_WATER && this.map[startIndex] !== this.SHALLOW_WATER)
		{
			this.navalPassMap[startIndex] = 1;	// impassable for naval
			return false;	// do nothing
		}
		floodFor = "water";
	}
	else if (this.map[startIndex] === this.DEEP_WATER)
	{
		this.landPassMap[startIndex] = 1;	// impassable for land
		return false;
	}

	// here we'll be able to start.
	for (let i = this.regionSize.length; i <= value; ++i)
	{
		this.regionLinks.push([]);
		this.regionSize.push(0);
		this.regionType.push("inaccessible");
	}
	let w = this.width;
	let h = this.height;

	let y = 0;
	// Get x and y from index
	let IndexArray = [startIndex];
	let newIndex;
	while(IndexArray.length)
	{
		newIndex = IndexArray.pop();

		y = 0;
		let loop = false;
		// vertical iteration
		do {
			--y;
			loop = false;
			let index = newIndex + w*y;
			if (index < 0)
				break;
			if (floodFor === "land" && this.landPassMap[index] === 0 && this.map[index] !== this.IMPASSABLE && this.map[index] !== this.DEEP_WATER)
				loop = true;
			else if (floodFor === "water" && this.navalPassMap[index] === 0 && (this.map[index] === this.DEEP_WATER || this.map[index] === this.SHALLOW_WATER))
				loop = true;
			else
				break;
		} while (loop === true);	// should actually break
		++y;
		let reachLeft = false;
		let reachRight = false;
		let index;
		do {
			index = newIndex + w*y;

			if (floodFor === "land" && this.landPassMap[index] === 0 && this.map[index] !== this.IMPASSABLE && this.map[index] !== this.DEEP_WATER)
			{
				this.landPassMap[index] = value;
				this.regionSize[value]++;
			}
			else if (floodFor === "water" && this.navalPassMap[index] === 0 && (this.map[index] === this.DEEP_WATER || this.map[index] === this.SHALLOW_WATER))
			{
				this.navalPassMap[index] = value;
				this.regionSize[value]++;
			}
			else
				break;

			if (index%w > 0)
			{
				if (floodFor === "land" && this.landPassMap[index -1] === 0 && this.map[index -1] !== this.IMPASSABLE && this.map[index -1] !== this.DEEP_WATER)
				{
					if (!reachLeft)
					{
						IndexArray.push(index -1);
						reachLeft = true;
					}
				}
				else if (floodFor === "water" && this.navalPassMap[index -1] === 0 && (this.map[index -1] === this.DEEP_WATER || this.map[index -1] === this.SHALLOW_WATER))
				{
					if (!reachLeft)
					{
						IndexArray.push(index -1);
						reachLeft = true;
					}
				}
				else if (reachLeft)
					reachLeft = false;
			}

			if (index%w < w - 1)
			{
				if (floodFor === "land" && this.landPassMap[index +1] === 0 && this.map[index +1] !== this.IMPASSABLE && this.map[index +1] !== this.DEEP_WATER)
				{
					if (!reachRight)
					{
						IndexArray.push(index +1);
						reachRight = true;
					}
				}
				else if (floodFor === "water" && this.navalPassMap[index +1] === 0 && (this.map[index +1] === this.DEEP_WATER || this.map[index +1] === this.SHALLOW_WATER))
				{
					if (!reachRight)
					{
						IndexArray.push(index +1);
						reachRight = true;
					}
				}
				else if (reachRight)
					reachRight = false;
			}
			++y;
		} while (index/w < h-1);	// should actually break
	}
	return true;
};

return m;

}(API3);