rot-js
Version:
A roguelike toolkit in JavaScript
322 lines (321 loc) • 11.3 kB
JavaScript
import Map from "./map.js";
import { DIRS } from "../constants.js";
import RNG from "../rng.js";
;
/**
* @class Cellular automaton map generator
* @augments ROT.Map
* @param {int} [width=ROT.DEFAULT_WIDTH]
* @param {int} [height=ROT.DEFAULT_HEIGHT]
* @param {object} [options] Options
* @param {int[]} [options.born] List of neighbor counts for a new cell to be born in empty space
* @param {int[]} [options.survive] List of neighbor counts for an existing cell to survive
* @param {int} [options.topology] Topology 4 or 6 or 8
*/
export default class Cellular extends Map {
constructor(width, height, options = {}) {
super(width, height);
this._options = {
born: [5, 6, 7, 8],
survive: [4, 5, 6, 7, 8],
topology: 8
};
this.setOptions(options);
this._dirs = DIRS[this._options.topology];
this._map = this._fillMap(0);
}
/**
* Fill the map with random values
* @param {float} probability Probability for a cell to become alive; 0 = all empty, 1 = all full
*/
randomize(probability) {
for (let i = 0; i < this._width; i++) {
for (let j = 0; j < this._height; j++) {
this._map[i][j] = (RNG.getUniform() < probability ? 1 : 0);
}
}
return this;
}
/**
* Change options.
* @see ROT.Map.Cellular
*/
setOptions(options) { Object.assign(this._options, options); }
set(x, y, value) { this._map[x][y] = value; }
create(callback) {
let newMap = this._fillMap(0);
let born = this._options.born;
let survive = this._options.survive;
for (let j = 0; j < this._height; j++) {
let widthStep = 1;
let widthStart = 0;
if (this._options.topology == 6) {
widthStep = 2;
widthStart = j % 2;
}
for (let i = widthStart; i < this._width; i += widthStep) {
let cur = this._map[i][j];
let ncount = this._getNeighbors(i, j);
if (cur && survive.indexOf(ncount) != -1) { /* survive */
newMap[i][j] = 1;
}
else if (!cur && born.indexOf(ncount) != -1) { /* born */
newMap[i][j] = 1;
}
}
}
this._map = newMap;
callback && this._serviceCallback(callback);
}
_serviceCallback(callback) {
for (let j = 0; j < this._height; j++) {
let widthStep = 1;
let widthStart = 0;
if (this._options.topology == 6) {
widthStep = 2;
widthStart = j % 2;
}
for (let i = widthStart; i < this._width; i += widthStep) {
callback(i, j, this._map[i][j]);
}
}
}
/**
* Get neighbor count at [i,j] in this._map
*/
_getNeighbors(cx, cy) {
let result = 0;
for (let i = 0; i < this._dirs.length; i++) {
let dir = this._dirs[i];
let x = cx + dir[0];
let y = cy + dir[1];
if (x < 0 || x >= this._width || y < 0 || y >= this._height) {
continue;
}
result += (this._map[x][y] == 1 ? 1 : 0);
}
return result;
}
/**
* Make sure every non-wall space is accessible.
* @param {function} callback to call to display map when do
* @param {int} value to consider empty space - defaults to 0
* @param {function} callback to call when a new connection is made
*/
connect(callback, value, connectionCallback) {
if (!value)
value = 0;
let allFreeSpace = [];
let notConnected = {};
// find all free space
let widthStep = 1;
let widthStarts = [0, 0];
if (this._options.topology == 6) {
widthStep = 2;
widthStarts = [0, 1];
}
for (let y = 0; y < this._height; y++) {
for (let x = widthStarts[y % 2]; x < this._width; x += widthStep) {
if (this._freeSpace(x, y, value)) {
let p = [x, y];
notConnected[this._pointKey(p)] = p;
allFreeSpace.push([x, y]);
}
}
}
let start = allFreeSpace[RNG.getUniformInt(0, allFreeSpace.length - 1)];
let key = this._pointKey(start);
let connected = {};
connected[key] = start;
delete notConnected[key];
// find what's connected to the starting point
this._findConnected(connected, notConnected, [start], false, value);
while (Object.keys(notConnected).length > 0) {
// find two points from notConnected to connected
let p = this._getFromTo(connected, notConnected);
let from = p[0]; // notConnected
let to = p[1]; // connected
// find everything connected to the starting point
let local = {};
local[this._pointKey(from)] = from;
this._findConnected(local, notConnected, [from], true, value);
// connect to a connected cell
let tunnelFn = (this._options.topology == 6 ? this._tunnelToConnected6 : this._tunnelToConnected);
tunnelFn.call(this, to, from, connected, notConnected, value, connectionCallback);
// now all of local is connected
for (let k in local) {
let pp = local[k];
this._map[pp[0]][pp[1]] = value;
connected[k] = pp;
delete notConnected[k];
}
}
callback && this._serviceCallback(callback);
}
/**
* Find random points to connect. Search for the closest point in the larger space.
* This is to minimize the length of the passage while maintaining good performance.
*/
_getFromTo(connected, notConnected) {
let from = [0, 0], to = [0, 0], d;
let connectedKeys = Object.keys(connected);
let notConnectedKeys = Object.keys(notConnected);
for (let i = 0; i < 5; i++) {
if (connectedKeys.length < notConnectedKeys.length) {
let keys = connectedKeys;
to = connected[keys[RNG.getUniformInt(0, keys.length - 1)]];
from = this._getClosest(to, notConnected);
}
else {
let keys = notConnectedKeys;
from = notConnected[keys[RNG.getUniformInt(0, keys.length - 1)]];
to = this._getClosest(from, connected);
}
d = (from[0] - to[0]) * (from[0] - to[0]) + (from[1] - to[1]) * (from[1] - to[1]);
if (d < 64) {
break;
}
}
// console.log(">>> connected=" + to + " notConnected=" + from + " dist=" + d);
return [from, to];
}
_getClosest(point, space) {
let minPoint = null;
let minDist = null;
for (let k in space) {
let p = space[k];
let d = (p[0] - point[0]) * (p[0] - point[0]) + (p[1] - point[1]) * (p[1] - point[1]);
if (minDist == null || d < minDist) {
minDist = d;
minPoint = p;
}
}
return minPoint;
}
_findConnected(connected, notConnected, stack, keepNotConnected, value) {
while (stack.length > 0) {
let p = stack.splice(0, 1)[0];
let tests;
if (this._options.topology == 6) {
tests = [
[p[0] + 2, p[1]],
[p[0] + 1, p[1] - 1],
[p[0] - 1, p[1] - 1],
[p[0] - 2, p[1]],
[p[0] - 1, p[1] + 1],
[p[0] + 1, p[1] + 1],
];
}
else {
tests = [
[p[0] + 1, p[1]],
[p[0] - 1, p[1]],
[p[0], p[1] + 1],
[p[0], p[1] - 1]
];
}
for (let i = 0; i < tests.length; i++) {
let key = this._pointKey(tests[i]);
if (connected[key] == null && this._freeSpace(tests[i][0], tests[i][1], value)) {
connected[key] = tests[i];
if (!keepNotConnected) {
delete notConnected[key];
}
stack.push(tests[i]);
}
}
}
}
_tunnelToConnected(to, from, connected, notConnected, value, connectionCallback) {
let a, b;
if (from[0] < to[0]) {
a = from;
b = to;
}
else {
a = to;
b = from;
}
for (let xx = a[0]; xx <= b[0]; xx++) {
this._map[xx][a[1]] = value;
let p = [xx, a[1]];
let pkey = this._pointKey(p);
connected[pkey] = p;
delete notConnected[pkey];
}
if (connectionCallback && a[0] < b[0]) {
connectionCallback(a, [b[0], a[1]]);
}
// x is now fixed
let x = b[0];
if (from[1] < to[1]) {
a = from;
b = to;
}
else {
a = to;
b = from;
}
for (let yy = a[1]; yy < b[1]; yy++) {
this._map[x][yy] = value;
let p = [x, yy];
let pkey = this._pointKey(p);
connected[pkey] = p;
delete notConnected[pkey];
}
if (connectionCallback && a[1] < b[1]) {
connectionCallback([b[0], a[1]], [b[0], b[1]]);
}
}
_tunnelToConnected6(to, from, connected, notConnected, value, connectionCallback) {
let a, b;
if (from[0] < to[0]) {
a = from;
b = to;
}
else {
a = to;
b = from;
}
// tunnel diagonally until horizontally level
let xx = a[0];
let yy = a[1];
while (!(xx == b[0] && yy == b[1])) {
let stepWidth = 2;
if (yy < b[1]) {
yy++;
stepWidth = 1;
}
else if (yy > b[1]) {
yy--;
stepWidth = 1;
}
if (xx < b[0]) {
xx += stepWidth;
}
else if (xx > b[0]) {
xx -= stepWidth;
}
else if (b[1] % 2) {
// Won't step outside map if destination on is map's right edge
xx -= stepWidth;
}
else {
// ditto for left edge
xx += stepWidth;
}
this._map[xx][yy] = value;
let p = [xx, yy];
let pkey = this._pointKey(p);
connected[pkey] = p;
delete notConnected[pkey];
}
if (connectionCallback) {
connectionCallback(from, to);
}
}
_freeSpace(x, y, value) {
return x >= 0 && x < this._width && y >= 0 && y < this._height && this._map[x][y] == value;
}
_pointKey(p) { return p[0] + "." + p[1]; }
}