Skip to content

Instantly share code, notes, and snippets.

Created December 7, 2016 21:04
Show Gist options
  • Save anonymous/f51587ffc33cb49edd4737324bf2516d to your computer and use it in GitHub Desktop.
Save anonymous/f51587ffc33cb49edd4737324bf2516d to your computer and use it in GitHub Desktop.
JS Bin // source http://jsbin.com/hipadem
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
<style id="jsbin-css">
body {
margin: 50px 25px 0 25px;
background-color: #ccc;
}
canvas {
float: left;
}
#stats {
float: left;
}
#tools {
clear: left;
padding: 0.1rem;
}
#tools a {
margin: 0 0.3rem 0 0;
font-size: 0.9rem;
font-family: sans-serif;
}
#tools a#selected {
background-color: red;
color: white;
}
#log {
background-color: white;
margin-top: 0.5rem;
}
#log span {
display: block;
font-family: monospace;
}
</style>
</head>
<body>
<canvas id="c" width="256" height="256"></canvas>
<div id="stats"></div>
<div id="tools"></div>
<div id="log"></div>
<script id="jsbin-javascript">
var cfg = {
cellSize: 9,
tileColors: [
function(v) { return 'rgb(255,255,255)'; },
function(v) {
var fv = Math.floor(map(v, 0, 1, 0, 225));
return 'rgb('+fv+','+fv+','+fv+')'; },
function(v) { return 'rgb(0,255,0)'; },
function(v) { return 'rgb(0,0,0)'; },
function(v) { return 'rgb(127,0,127)'; },
],
jobSpeed: 250,
gridWidth: 32,
gridHeight: 32,
gridAreal: 32 * 32
};
var V_TYPE = {
VACUUM: 0,
ROOM: 1,
PRODUCER: 2,
WALL: 3,
REPAIR: 4,
};
var logs = [];
var logContainer;
var statsContainer;
var jobsGrow = [];
var jobsShrink = [];
var jobsRepair = [];
var tiles = [];
var curTool;
var canvas = document.getElementById('c');
var ctx = canvas.getContext('2d');
var offsetsDiag = [
{x:-1, y:1}, {x:-1, y:-1}, {x:1, y:-1}, {x:1, y:1}
];
var offsetsVonN = [
{x:0, y:1}, {x:-1, y:0}, {x:0, y:-1}, {x:1, y:0}
];
var offsetsMoore = [
{x:0, y:1}, {x:-1, y:1}, {x:-1, y:0},
{x:-1, y:-1}, {x:0, y:-1}, {x:1, y:-1},
{x:1, y:0}, {x:1, y:1}
];
var tools = {
vacuum: {
callback: function(tile) {
if (tile.t === V_TYPE.PRODUCER ||
tile.t === V_TYPE.ROOM ||
tile.t === V_TYPE.VACUUM) {
return;
}
addVacuum(tile);
}
},
room: {
callback: function(tile) {
if (tile.t === V_TYPE.PRODUCER) {
return;
}
var neighbours = getOrderedNeighboursTo(tile.x, tile.y);
for (var j=0; j<neighbours.length; j++) {
var ni = neighbours[j];
if (ni === -1) {
continue;
}
if (tiles[ni].t !== V_TYPE.VACUUM) {
continue;
}
tiles[ni].t = V_TYPE.WALL;
}
// If any of the neighbours are vacuum, consume
// the tile by vacuum.
/*
var vacuum = false;
eachNeighbour(tile, function(ntile, n, i) {
if (ntile.t === V_TYPE.VACUUM) {
vacuum = true;
}
});
if (vacuum) {
tile.t = V_TYPE.ROOM;
fillVacuum(tile);
return;
}
*/
addRoom(tile);
// Otherwise, add room and continue the
// grow algorithm if any
var ns = getNeighboursTo(tile.x, tile.y);
var max = 0;
var m = -1;
for (var i=0; i<ns.length; i++) {
var n = ns[i];
if (tiles[n].v > max) {
max = tiles[n].v;
m = i;
}
}
if (max > 0) {
growFrom(tiles[ns[m]]);
}
}
},
producer: {
callback: function(tile) {
if (tile.t === V_TYPE.PRODUCER) {
tile.t = V_TYPE.ROOM;
tile.v = 7;
shrinkFrom(tile);
return
}
if (tile.t === V_TYPE.ROOM ||
tile.t === V_TYPE.VACUUM) {
tile.t = V_TYPE.PRODUCER;
tile.v = 8;
growFrom(tile);
return;
}
}
},
wall: {
callback: function(tile) {
if (tile.t === V_TYPE.PRODUCER) {
return;
}
// When repairing a wall
if (tile.t === V_TYPE.VACUUM) {
// If tile has two adjacent neighbours, either NS or EW,
// consider it a repair
var sig = getSignature(tile);
if (sig === 5 || sig === 10) {
console.log('repair');
var neighs = getNeighboursTo(tile.x, tile.y);
for (var i=0; i<neighs.length; i++) {
var n = neighs[i];
var nt = tiles[n];
var v = net.findVertex(nt.x, nt.y);
console.log(nt.x, nt.y, 'has vertex', v);
if (v < 0) {
continue;
}
console.log('fill from', nt.x, nt.y, '(',tile.x, tile.y,')');
floodFill(nt.x, nt.y, V_TYPE.ROOM);
net.addEdgesToTile(nt);
net.addFacesToTile(nt);
updateStats();
// look for neighbours that are of type ROOM
// if (tiles[n].t !== V_TYPE.ROOM) {
// continue;
// }
// var ok = repairFill(nt.x, nt.y);
// log("ok "+ok);
// if (ok) {
tile.t = V_TYPE.WALL;
tile.v = 0;
// }
}
return;
}
}
tile.t = V_TYPE.WALL;
tile.v = 0;
var min = 8;
var ni = -1;
eachNeighbour(tile, function(n, i) {
if (n.t !== V_TYPE.ROOM &&
n.t !== V_TYPE.PRODUCER) {
return;
}
if (n.t === V_TYPE.ROOM) {
net.addEdgesToTile(n);
net.addFacesToTile(n);
updateStats();
}
if (n.v < min) {
min = n.v;
ni = flattenIndex(n.x, n.y);
}
});
if (min === 0 || min === 8) {
return;
}
var top = crawlUp(tiles[ni]);
if (top.v < 8) {
shrinkFrom(top);
}
}
},
};
var net = {
verts: [],
edges: [],
faces: [],
// bitmasks for illegal combos. There are only three (or two
// in this case):
// - when there are exactly three neighbours, and they are all
// on line, either along the x or y axis. So something like:
// 1010 and 0101
illegals: [],
getNeighbours: function(x, y) {
var neighbours = [];
neighbours.push(this.findVertex(x, y+1)); // S
neighbours.push(this.findVertex(x-1, y )); // W
neighbours.push(this.findVertex(x , y-1)); // N
neighbours.push(this.findVertex(x+1, y )); // E
return neighbours;
},
addMooreNeighbours: function(list, tile, offsets) {
for (var n=0, o=1; n<offsets.length; n++, o+=2) {
var v = this.findVertex(tile.x + offsets[n].x,
tile.y + offsets[n].y);
list.splice(o, 0, v);
}
},
addVertex: function(tile) {
this.verts.push({x: tile.x, y: tile.y});
var v = this.verts.length - 1;
var neighbours = this.getNeighbours(tile.x, tile.y);
this.addEdges(v, neighbours);
this.addMooreNeighbours(neighbours, tile, offsetsDiag);
this.addFaces(v, neighbours);
},
findVertex: function(x, y) {
for (var i=0; i<this.verts.length; i++) {
if (this.verts[i].x === x &&
this.verts[i].y === y) {
return i;
}
}
return -1;
},
addEdgesToTile: function(tile) {
var v = this.findVertex(tile.x, tile.y);
var neighbours = this.getNeighbours(tile.x, tile.y);
this.addEdges(v, neighbours);
},
addEdges: function(fromVert, neighbours) {
function valid(n) {
return n > -1;
}
var nlist = neighbours.filter(valid);
for (var i=0; i<nlist.length; i++) {
this.edges.push(fromVert);
this.edges.push(nlist[i]);
}
},
findEdges: function(v) {
var edges = [];
for (var i=0; i<this.edges.length; i+=2) {
if (this.edges[i] === v ||
this.edges[i+1] === v) {
edges.push(i);
}
}
return edges;
},
removeEdges: function(tile) {
var v = this.findVertex(tile.x, tile.y);
var edges = this.findEdges(v);
for (var i=edges.length-1; i>=0; i--) {
this.edges.splice(edges[i], 2);
}
},
addFacesToTile: function(tile) {
var v = this.findVertex(tile.x, tile.y);
var neighbours = this.getNeighbours(tile.x, tile.y);
this.addMooreNeighbours(neighbours, tile, offsetsDiag);
this.addFaces(v, neighbours);
},
addFaces: function(fromVert, neighbours) {
var exists = function(a, b, c) {
return this[a] > -1 &&
this[b] > -1 &&
this[c] > -1;
}.bind(neighbours);
var create = function(a, b, c) {
if (exists(a, b, c)) {
this.faces.push(fromVert);
this.faces.push(neighbours[a]);
this.faces.push(neighbours[b]);
this.faces.push(neighbours[c]);
}
}.bind(this);
create(0,1,2);
create(2,3,4);
create(4,5,6);
create(6,7,0);
},
findFaces: function(v) {
var faces = [];
for (var i=0; i<this.faces.length; i+=4) {
if (this.faces[i] === v ||
this.faces[i+1] === v ||
this.faces[i+2] === v ||
this.faces[i+3] === v) {
faces.push(i);
}
}
return faces;
},
removeFaces: function(tile) {
var v = this.findVertex(tile.x, tile.y);
var faces = this.findFaces(v);
for (var i=faces.length-1; i>=0; i--) {
this.faces.splice(faces[i], 4);
}
},
};
function addVacuum(tile) {
if (tile.t === V_TYPE.WALL) {
eachNeighbour(tile, function(n, i) {
if (n.t === V_TYPE.ROOM) {
net.removeEdges(n);
net.removeFaces(n);
updateStats();
}
});
}
eachNeighbour(tile, function(n, i) {
if (n.t === V_TYPE.ROOM) {
fillVacuum(n);
}
});
tile.t = V_TYPE.VACUUM;
}
function addRoom(tile) {
tile.t = V_TYPE.ROOM;
net.addVertex(tile);
updateStats();
// network.addVertex(tile.x, tile.y)
// - check neighbours (look in vertices list)
// - add edges and faces
// num neighbours
// 0: add its own vertex to network mesh
// 1: add vertex, 1 edge
// 2: add vertex, 2 edges, 1 face
// 3: add vertex, 3 edges, 2 faces
// 4: add vertex, 4 edges, 3 faces
// edges = neighbours
// faces = edges - 1
}
function updateStats() {
var v = net.verts.length;
var e = net.edges.length / 2;
var f = net.faces.length / 4;
var state = v - e + f;
stat((state === 1 ? 'closed' : 'open') + ' ('+v+', '+e+', '+f+') '+state);
}
function drawRect(x, y, w, h, t) {
drawLine([x, y], w, 0, t);
drawLine([x, y+1], h-2, 1, t);
drawLine([x+w-1, y+1], h-2, 1, t);
drawLine([x, y+h-1], w, 0, t);
}
// dir is either 0 or 1, depending on the direction
// you want to draw. 0 = x and 1 = y
function drawLine(coord, len, dir, t) {
for (var i=0; i<len; i++) {
var n = flattenIndex(coord[0], coord[1]);
tiles[n].t = t;
coord[dir]++;
}
}
function drawDot(x, y, t) {
var n = flattenIndex(x, y);
tiles[n].t = t;
}
function floodFill(x, y, t, cb) {
var fi = flattenIndex(x, y);
var fill_type = tiles[fi].t;
var fill = [];
fill.push(tiles[fi]);
while (fill.length > 0) {
var tile = fill.shift();
if (tile.t === t) {
continue;
}
tile.t = t;
if (typeof cb !== 'undefined') {
cb(tile);
}
var neighbours = getNeighboursTo(tile.x, tile.y);
for (var i=0; i<neighbours.length; i++) {
var n = neighbours[i];
if (tiles[n].t === fill_type) {
fill.push(tiles[n]);
}
}
}
}
function repairFill(x, y) {
var fi = flattenIndex(x, y);
var fill = [];
fill.push(tiles[fi]);
var success = true;
while (fill.length > 0) {
var tile = fill.shift();
if (tile.t === V_TYPE.VACUUM) {
success = false;
break;
}
tile.t = V_TYPE.REPAIR;
var neighbours = getNeighboursTo(tile.x, tile.y);
for (var i=0; i<neighbours.length; i++) {
var n = neighbours[i];
if (tiles[n].t === V_TYPE.ROOM) {
fill.push(tiles[n]);
}
}
}
return success;
}
function crawlUp(tile) {
var ti = flattenIndex(tile.x, tile.y);
var list = [];
list.push(tiles[ti]);
var top = 0;
while (list.length > 0) {
var t = list.shift();
if (t.v === 0 || t.v === 8) {
break;
}
var max = 0;
var ni = -1;
var neighbours = getNeighboursTo(t.x, t.y);
for (var i=0; i<neighbours.length; i++) {
var n = neighbours[i];
if (tiles[n].v > max) {
max = tiles[n].v;
ni = n;
}
}
if (max > 0) {
top = max;
ti = ni;
list.push(tiles[ni]);
}
}
return tiles[ti];
}
function fillVacuum(tile) {
floodFill(tile.x,
tile.y,
V_TYPE.VACUUM,
function(t) { t.a = -1; });
}
function flattenIndex(x, y) {
return x + cfg.gridWidth * y;
}
function setTiles(callback) {
var x=0;
var y=0;
for (var i=0; i<cfg.gridAreal; i++) {
x = (x + 1) % cfg.gridWidth;
y = Math.floor(i / cfg.gridHeight);
var n = flattenIndex(x,y);
tiles[n] = callback(tiles[n], x, y, i);
}
}
function fillTiles() {
setTiles(function(tile, x, y, i) {
return {x:x, y:y, t:V_TYPE.VACUUM, v:0, a:0};
});
}
function clear() {
ctx.fillStyle = 'rgb(255,255,255)';
ctx.fillRect(0, 0,
cfg.cellSize * cfg.gridWidth,
cfg.cellSize * cfg.gridHeight);
}
function getTile(event, cellSize) {
return {
x: Math.floor(event.offsetX / cellSize),
y: Math.floor(event.offsetY / cellSize)
};
}
function render() {
clear();
var scale = function(val) {
return {
x: val.x * cfg.cellSize + cfg.cellSize * 0.5,
y: val.y * cfg.cellSize + cfg.cellSize * 0.5,
}
}
var i;
for (i=0; i<cfg.gridAreal; i++) {
var tile = tiles[i];
var color = cfg.tileColors[tile.t];
ctx.fillStyle = color(1 - (tile.v / 8));
ctx.fillRect(tile.x * cfg.cellSize,
tile.y * cfg.cellSize,
cfg.cellSize,
cfg.cellSize);
}
for (i=0; i<net.verts.length; i++) {
var vert = net.verts[i];
ctx.fillStyle = 'rgb(0, 0, 0)';
ctx.fillRect(scale(vert.x), scale(vert.y), 1, 1);
}
ctx.beginPath();
for (i=0; i<net.edges.length; i+=2) {
var fv = net.verts[net.edges[i]];
var tv = net.verts[net.edges[i+1]];
fv = scale(fv);
tv = scale(tv);
ctx.moveTo(fv.x, fv.y);
ctx.lineTo(tv.x, tv.y);
}
ctx.stroke();
}
// A tile's signature is a bitmask telling
// which sides of the tile are walls
function getSignature(tile) {
var neighbours = getOrderedNeighboursTo(tile.x, tile.y);
var sig = 0;
for (var i=0; i<neighbours.length; i++) {
var n = neighbours[i];
if (typeof n === 'undefined') {
continue;
}
if (tiles[n].t === V_TYPE.WALL) {
sig += 1 << i;
}
}
return sig;
}
function countNeighbours(tile) {
var neighbours = getNeighboursTo(tile.x, tile.y);
var count = 0;
for (var i=0; i<neighbours.length; i++) {
var n = neighbours[i];
count += clamp01(tiles[n].t);
}
return count;
}
function getNeighboursBySig(tile, sig) {
var neighbours = [];
var dirs = [{x:0,y:1},{x:-1,y:0},{x:0,y:-1},{x:1,y:0}];
for (var i=1, n=0; i<16; i*=2, n++) {
if (sig & i === 0) {
continue;
}
var x = tile.x + dirs[n].x;
var y = tile.y + dirs[n].y;
var j = flattenIndex(x, y);
if (j < 0 || j >= tiles.length) {
continue;
}
neighbours[n] = tiles[j];
}
return neighbours;
}
// Returns a list of neighbours within the
// Von Neumann neighbourhood. The list is
// not ordered on neighbourhood
function getNeighboursTo(x, y) {
var addNeighbour = function(x, y, list) {
var i = flattenIndex(x, y);
if (i >= 0 && i < tiles.length) {
list.push(i);
}
}
var neighbours = [];
addNeighbour(x, y+1, neighbours); // S
addNeighbour(x-1, y , neighbours); // W
addNeighbour(x, y-1, neighbours); // N
addNeighbour(x+1, y , neighbours); // E
return neighbours;
}
function getOrderedNeighboursTo(x, y) {
var getNeighbour = function(x, y) {
var i = flattenIndex(x, y);
if (i >= 0 && i < tiles.length) {
return i;
}
return -1;
}
var neighbours = [];
neighbours.push(getNeighbour(x, y+1)); // S
neighbours.push(getNeighbour(x-1, y )); // W
neighbours.push(getNeighbour(x, y-1)); // N
neighbours.push(getNeighbour(x+1, y )); // E
return neighbours;
}
function getOrderedMooreNeighboursTo(x, y) {
var getNeighbour = function(x, y) {
var i = flattenIndex(x, y);
if (i >= 0 && i < tiles.length) {
return i;
}
return -1;
}
var neighbours = [];
neighbours.push(getNeighbour(x, y+1)); // S
neighbours.push(getNeighbour(x-1, y+1)); // SW
neighbours.push(getNeighbour(x-1, y )); // W
neighbours.push(getNeighbour(x-1, y-1)); // NW
neighbours.push(getNeighbour(x, y-1)); // N
neighbours.push(getNeighbour(x+1, y-1)); // NE
neighbours.push(getNeighbour(x+1, y )); // E
neighbours.push(getNeighbour(x+1, y+1)); // SE
return neighbours;
}
function eachNeighbour(tile, cb) {
var neighbours = getNeighboursTo(tile.x, tile.y);
for (var i=0; i<neighbours.length; i++) {
var n = neighbours[i];
cb(tiles[n], i, n);
}
}
function processJobs(jobs, cb) {
var numJobs = jobs.length;
var counter = 0;
while (counter < numJobs) {
cb(jobs.shift());
counter++;
}
}
function growFrom(tile) {
jobsGrow.push(tile);
window.setTimeout(grow, cfg.jobSpeed);
}
function grow() {
processJobs(jobsGrow, function(tile) {
var neighbours = getNeighboursTo(tile.x, tile.y);
for (var i=0; i<neighbours.length; i++) {
var n = neighbours[i];
// We don't care about neighbours that are greater
// or equal, or exactly one value lower than the
// current tile.
var diff = tiles[n].v - tile.v;
if (diff >= 0 || diff === -1) {
continue;
}
// Ignore walls
if (tiles[n].t === V_TYPE.WALL) {
continue;
}
tiles[n].v = tile.v - 1;
if (tiles[n].v > 1) {
jobsGrow.push(tiles[n]);
}
}
});
render();
if (jobsGrow.length > 0) {
window.setTimeout(grow, cfg.jobSpeed);
}
}
function shrinkFrom(tile) {
var addJob = function(tile) {
var neighbours = getNeighboursTo(tile.x, tile.y);
for (var i=0; i<neighbours.length; i++) {
jobsShrink.push(tiles[neighbours[i]]);
}
jobsShrink.push(tile);
}
if (tile instanceof Array) {
for (var t=0; t<tile.length; t++) {
addJob(tile[t]);
}
} else {
addJob(tile);
}
window.setTimeout(shrink, cfg.jobSpeed);
}
function shrink() {
var moreJobs = [];
processJobs(jobsShrink, function(tile) {
var neighbours = getNeighboursTo(tile.x, tile.y);
var v = {v:0};
for (var i=0; i<neighbours.length; i++) {
var ni = neighbours[i];
if (tiles[ni].v > v.v) {
v = tiles[ni];
}
}
if (v.v <= tile.v) {
var newVal = v.v - 1;
if (newVal >= 0) {
tile.v = newVal;
moreJobs.push(tile);
} else {
tile.v = 0;
}
}
});
render();
shrinkFrom(moreJobs);
}
canvas.addEventListener('click', function(event) {
var click = getTile(event, cfg.cellSize);
var i = flattenIndex(click.x, click.y);
var tile = tiles[i];
tools[curTool].callback(tile);
render();
});
function registerTools(tools, defaultTool) {
var container = document.getElementById('tools');
Object.keys(tools).forEach(function(tool) {
var a = document.createElement('a');
a.innerHTML= tool;
a.dataset.tool = tool;
container.appendChild(a);
tools[tool].element = a;
});
container.addEventListener('click', function(event) {
setTool(event.target.dataset.tool);
});
setTool(defaultTool);
}
function setTool(tool) {
curTool = tool;
Object.keys(tools).forEach(function(tool) {
var elm = tools[tool].element;
elm.id = elm.dataset.tool == curTool ? 'selected' : '';
});
}
function log(str) {
logs.push(str);
var diff = Math.max(logs.length - 20, 0);
logs.splice(0, diff);
while (logContainer.firstChild) {
logContainer.removeChild(logContainer.firstChild);
}
for (var i=0; i<logs.length; i++) {
var l = document.createElement('span');
l.innerHTML = logs[i];
logContainer.appendChild(l);
}
}
function stat(val) {
statsContainer.innerHTML = val;
}
function clamp01(val) {
return clamp(val, 0, 1);
}
function clamp(val, min, max) {
return Math.min(Math.max(val, min), max);
}
function map(val, mina, maxa, minb, maxb) {
return minb + (maxb - minb) * ((val - mina) / (maxa - mina));
}
canvas.width = cfg.cellSize * cfg.gridWidth;
canvas.height = cfg.cellSize * cfg.gridHeight;
fillTiles();
/*
drawRect(4, 4, 8, 8, 3);
drawRect(11, 4, 5, 5, 3);
drawRect(4, 11, 8, 8, 3);
drawRect(11, 11, 12, 12, 3);
drawDot(6, 11, 0);
//drawDot(11, 14, 0);
floodFill(5, 5, V_TYPE.ROOM);
*/
render();
registerTools(tools, 'room');
logContainer = document.getElementById('log');
statsContainer = document.getElementById('stats');
</script>
<script id="jsbin-source-css" type="text/css">body {
margin: 50px 25px 0 25px;
background-color: #ccc;
}
canvas {
float: left;
}
#stats {
float: left;
}
#tools {
clear: left;
padding: 0.1rem;
}
#tools a {
margin: 0 0.3rem 0 0;
font-size: 0.9rem;
font-family: sans-serif;
}
#tools a#selected {
background-color: red;
color: white;
}
#log {
background-color: white;
margin-top: 0.5rem;
}
#log span {
display: block;
font-family: monospace;
}</script>
<script id="jsbin-source-javascript" type="text/javascript">var cfg = {
cellSize: 9,
tileColors: [
function(v) { return 'rgb(255,255,255)'; },
function(v) {
var fv = Math.floor(map(v, 0, 1, 0, 225));
return 'rgb('+fv+','+fv+','+fv+')'; },
function(v) { return 'rgb(0,255,0)'; },
function(v) { return 'rgb(0,0,0)'; },
function(v) { return 'rgb(127,0,127)'; },
],
jobSpeed: 250,
gridWidth: 32,
gridHeight: 32,
gridAreal: 32 * 32
};
var V_TYPE = {
VACUUM: 0,
ROOM: 1,
PRODUCER: 2,
WALL: 3,
REPAIR: 4,
};
var logs = [];
var logContainer;
var statsContainer;
var jobsGrow = [];
var jobsShrink = [];
var jobsRepair = [];
var tiles = [];
var curTool;
var canvas = document.getElementById('c');
var ctx = canvas.getContext('2d');
var offsetsDiag = [
{x:-1, y:1}, {x:-1, y:-1}, {x:1, y:-1}, {x:1, y:1}
];
var offsetsVonN = [
{x:0, y:1}, {x:-1, y:0}, {x:0, y:-1}, {x:1, y:0}
];
var offsetsMoore = [
{x:0, y:1}, {x:-1, y:1}, {x:-1, y:0},
{x:-1, y:-1}, {x:0, y:-1}, {x:1, y:-1},
{x:1, y:0}, {x:1, y:1}
];
var tools = {
vacuum: {
callback: function(tile) {
if (tile.t === V_TYPE.PRODUCER ||
tile.t === V_TYPE.ROOM ||
tile.t === V_TYPE.VACUUM) {
return;
}
addVacuum(tile);
}
},
room: {
callback: function(tile) {
if (tile.t === V_TYPE.PRODUCER) {
return;
}
var neighbours = getOrderedNeighboursTo(tile.x, tile.y);
for (var j=0; j<neighbours.length; j++) {
var ni = neighbours[j];
if (ni === -1) {
continue;
}
if (tiles[ni].t !== V_TYPE.VACUUM) {
continue;
}
tiles[ni].t = V_TYPE.WALL;
}
// If any of the neighbours are vacuum, consume
// the tile by vacuum.
/*
var vacuum = false;
eachNeighbour(tile, function(ntile, n, i) {
if (ntile.t === V_TYPE.VACUUM) {
vacuum = true;
}
});
if (vacuum) {
tile.t = V_TYPE.ROOM;
fillVacuum(tile);
return;
}
*/
addRoom(tile);
// Otherwise, add room and continue the
// grow algorithm if any
var ns = getNeighboursTo(tile.x, tile.y);
var max = 0;
var m = -1;
for (var i=0; i<ns.length; i++) {
var n = ns[i];
if (tiles[n].v > max) {
max = tiles[n].v;
m = i;
}
}
if (max > 0) {
growFrom(tiles[ns[m]]);
}
}
},
producer: {
callback: function(tile) {
if (tile.t === V_TYPE.PRODUCER) {
tile.t = V_TYPE.ROOM;
tile.v = 7;
shrinkFrom(tile);
return
}
if (tile.t === V_TYPE.ROOM ||
tile.t === V_TYPE.VACUUM) {
tile.t = V_TYPE.PRODUCER;
tile.v = 8;
growFrom(tile);
return;
}
}
},
wall: {
callback: function(tile) {
if (tile.t === V_TYPE.PRODUCER) {
return;
}
// When repairing a wall
if (tile.t === V_TYPE.VACUUM) {
// If tile has two adjacent neighbours, either NS or EW,
// consider it a repair
var sig = getSignature(tile);
if (sig === 5 || sig === 10) {
console.log('repair');
var neighs = getNeighboursTo(tile.x, tile.y);
for (var i=0; i<neighs.length; i++) {
var n = neighs[i];
var nt = tiles[n];
var v = net.findVertex(nt.x, nt.y);
console.log(nt.x, nt.y, 'has vertex', v);
if (v < 0) {
continue;
}
console.log('fill from', nt.x, nt.y, '(',tile.x, tile.y,')');
floodFill(nt.x, nt.y, V_TYPE.ROOM);
net.addEdgesToTile(nt);
net.addFacesToTile(nt);
updateStats();
// look for neighbours that are of type ROOM
// if (tiles[n].t !== V_TYPE.ROOM) {
// continue;
// }
// var ok = repairFill(nt.x, nt.y);
// log("ok "+ok);
// if (ok) {
tile.t = V_TYPE.WALL;
tile.v = 0;
// }
}
return;
}
}
tile.t = V_TYPE.WALL;
tile.v = 0;
var min = 8;
var ni = -1;
eachNeighbour(tile, function(n, i) {
if (n.t !== V_TYPE.ROOM &&
n.t !== V_TYPE.PRODUCER) {
return;
}
if (n.t === V_TYPE.ROOM) {
net.addEdgesToTile(n);
net.addFacesToTile(n);
updateStats();
}
if (n.v < min) {
min = n.v;
ni = flattenIndex(n.x, n.y);
}
});
if (min === 0 || min === 8) {
return;
}
var top = crawlUp(tiles[ni]);
if (top.v < 8) {
shrinkFrom(top);
}
}
},
};
var net = {
verts: [],
edges: [],
faces: [],
// bitmasks for illegal combos. There are only three (or two
// in this case):
// - when there are exactly three neighbours, and they are all
// on line, either along the x or y axis. So something like:
// 1010 and 0101
illegals: [],
getNeighbours: function(x, y) {
var neighbours = [];
neighbours.push(this.findVertex(x, y+1)); // S
neighbours.push(this.findVertex(x-1, y )); // W
neighbours.push(this.findVertex(x , y-1)); // N
neighbours.push(this.findVertex(x+1, y )); // E
return neighbours;
},
addMooreNeighbours: function(list, tile, offsets) {
for (var n=0, o=1; n<offsets.length; n++, o+=2) {
var v = this.findVertex(tile.x + offsets[n].x,
tile.y + offsets[n].y);
list.splice(o, 0, v);
}
},
addVertex: function(tile) {
this.verts.push({x: tile.x, y: tile.y});
var v = this.verts.length - 1;
var neighbours = this.getNeighbours(tile.x, tile.y);
this.addEdges(v, neighbours);
this.addMooreNeighbours(neighbours, tile, offsetsDiag);
this.addFaces(v, neighbours);
},
findVertex: function(x, y) {
for (var i=0; i<this.verts.length; i++) {
if (this.verts[i].x === x &&
this.verts[i].y === y) {
return i;
}
}
return -1;
},
addEdgesToTile: function(tile) {
var v = this.findVertex(tile.x, tile.y);
var neighbours = this.getNeighbours(tile.x, tile.y);
this.addEdges(v, neighbours);
},
addEdges: function(fromVert, neighbours) {
function valid(n) {
return n > -1;
}
var nlist = neighbours.filter(valid);
for (var i=0; i<nlist.length; i++) {
this.edges.push(fromVert);
this.edges.push(nlist[i]);
}
},
findEdges: function(v) {
var edges = [];
for (var i=0; i<this.edges.length; i+=2) {
if (this.edges[i] === v ||
this.edges[i+1] === v) {
edges.push(i);
}
}
return edges;
},
removeEdges: function(tile) {
var v = this.findVertex(tile.x, tile.y);
var edges = this.findEdges(v);
for (var i=edges.length-1; i>=0; i--) {
this.edges.splice(edges[i], 2);
}
},
addFacesToTile: function(tile) {
var v = this.findVertex(tile.x, tile.y);
var neighbours = this.getNeighbours(tile.x, tile.y);
this.addMooreNeighbours(neighbours, tile, offsetsDiag);
this.addFaces(v, neighbours);
},
addFaces: function(fromVert, neighbours) {
var exists = function(a, b, c) {
return this[a] > -1 &&
this[b] > -1 &&
this[c] > -1;
}.bind(neighbours);
var create = function(a, b, c) {
if (exists(a, b, c)) {
this.faces.push(fromVert);
this.faces.push(neighbours[a]);
this.faces.push(neighbours[b]);
this.faces.push(neighbours[c]);
}
}.bind(this);
create(0,1,2);
create(2,3,4);
create(4,5,6);
create(6,7,0);
},
findFaces: function(v) {
var faces = [];
for (var i=0; i<this.faces.length; i+=4) {
if (this.faces[i] === v ||
this.faces[i+1] === v ||
this.faces[i+2] === v ||
this.faces[i+3] === v) {
faces.push(i);
}
}
return faces;
},
removeFaces: function(tile) {
var v = this.findVertex(tile.x, tile.y);
var faces = this.findFaces(v);
for (var i=faces.length-1; i>=0; i--) {
this.faces.splice(faces[i], 4);
}
},
};
function addVacuum(tile) {
if (tile.t === V_TYPE.WALL) {
eachNeighbour(tile, function(n, i) {
if (n.t === V_TYPE.ROOM) {
net.removeEdges(n);
net.removeFaces(n);
updateStats();
}
});
}
eachNeighbour(tile, function(n, i) {
if (n.t === V_TYPE.ROOM) {
fillVacuum(n);
}
});
tile.t = V_TYPE.VACUUM;
}
function addRoom(tile) {
tile.t = V_TYPE.ROOM;
net.addVertex(tile);
updateStats();
// network.addVertex(tile.x, tile.y)
// - check neighbours (look in vertices list)
// - add edges and faces
// num neighbours
// 0: add its own vertex to network mesh
// 1: add vertex, 1 edge
// 2: add vertex, 2 edges, 1 face
// 3: add vertex, 3 edges, 2 faces
// 4: add vertex, 4 edges, 3 faces
// edges = neighbours
// faces = edges - 1
}
function updateStats() {
var v = net.verts.length;
var e = net.edges.length / 2;
var f = net.faces.length / 4;
var state = v - e + f;
stat((state === 1 ? 'closed' : 'open') + ' ('+v+', '+e+', '+f+') '+state);
}
function drawRect(x, y, w, h, t) {
drawLine([x, y], w, 0, t);
drawLine([x, y+1], h-2, 1, t);
drawLine([x+w-1, y+1], h-2, 1, t);
drawLine([x, y+h-1], w, 0, t);
}
// dir is either 0 or 1, depending on the direction
// you want to draw. 0 = x and 1 = y
function drawLine(coord, len, dir, t) {
for (var i=0; i<len; i++) {
var n = flattenIndex(coord[0], coord[1]);
tiles[n].t = t;
coord[dir]++;
}
}
function drawDot(x, y, t) {
var n = flattenIndex(x, y);
tiles[n].t = t;
}
function floodFill(x, y, t, cb) {
var fi = flattenIndex(x, y);
var fill_type = tiles[fi].t;
var fill = [];
fill.push(tiles[fi]);
while (fill.length > 0) {
var tile = fill.shift();
if (tile.t === t) {
continue;
}
tile.t = t;
if (typeof cb !== 'undefined') {
cb(tile);
}
var neighbours = getNeighboursTo(tile.x, tile.y);
for (var i=0; i<neighbours.length; i++) {
var n = neighbours[i];
if (tiles[n].t === fill_type) {
fill.push(tiles[n]);
}
}
}
}
function repairFill(x, y) {
var fi = flattenIndex(x, y);
var fill = [];
fill.push(tiles[fi]);
var success = true;
while (fill.length > 0) {
var tile = fill.shift();
if (tile.t === V_TYPE.VACUUM) {
success = false;
break;
}
tile.t = V_TYPE.REPAIR;
var neighbours = getNeighboursTo(tile.x, tile.y);
for (var i=0; i<neighbours.length; i++) {
var n = neighbours[i];
if (tiles[n].t === V_TYPE.ROOM) {
fill.push(tiles[n]);
}
}
}
return success;
}
function crawlUp(tile) {
var ti = flattenIndex(tile.x, tile.y);
var list = [];
list.push(tiles[ti]);
var top = 0;
while (list.length > 0) {
var t = list.shift();
if (t.v === 0 || t.v === 8) {
break;
}
var max = 0;
var ni = -1;
var neighbours = getNeighboursTo(t.x, t.y);
for (var i=0; i<neighbours.length; i++) {
var n = neighbours[i];
if (tiles[n].v > max) {
max = tiles[n].v;
ni = n;
}
}
if (max > 0) {
top = max;
ti = ni;
list.push(tiles[ni]);
}
}
return tiles[ti];
}
function fillVacuum(tile) {
floodFill(tile.x,
tile.y,
V_TYPE.VACUUM,
function(t) { t.a = -1; });
}
function flattenIndex(x, y) {
return x + cfg.gridWidth * y;
}
function setTiles(callback) {
var x=0;
var y=0;
for (var i=0; i<cfg.gridAreal; i++) {
x = (x + 1) % cfg.gridWidth;
y = Math.floor(i / cfg.gridHeight);
var n = flattenIndex(x,y);
tiles[n] = callback(tiles[n], x, y, i);
}
}
function fillTiles() {
setTiles(function(tile, x, y, i) {
return {x:x, y:y, t:V_TYPE.VACUUM, v:0, a:0};
});
}
function clear() {
ctx.fillStyle = 'rgb(255,255,255)';
ctx.fillRect(0, 0,
cfg.cellSize * cfg.gridWidth,
cfg.cellSize * cfg.gridHeight);
}
function getTile(event, cellSize) {
return {
x: Math.floor(event.offsetX / cellSize),
y: Math.floor(event.offsetY / cellSize)
};
}
function render() {
clear();
var scale = function(val) {
return {
x: val.x * cfg.cellSize + cfg.cellSize * 0.5,
y: val.y * cfg.cellSize + cfg.cellSize * 0.5,
}
}
var i;
for (i=0; i<cfg.gridAreal; i++) {
var tile = tiles[i];
var color = cfg.tileColors[tile.t];
ctx.fillStyle = color(1 - (tile.v / 8));
ctx.fillRect(tile.x * cfg.cellSize,
tile.y * cfg.cellSize,
cfg.cellSize,
cfg.cellSize);
}
for (i=0; i<net.verts.length; i++) {
var vert = net.verts[i];
ctx.fillStyle = 'rgb(0, 0, 0)';
ctx.fillRect(scale(vert.x), scale(vert.y), 1, 1);
}
ctx.beginPath();
for (i=0; i<net.edges.length; i+=2) {
var fv = net.verts[net.edges[i]];
var tv = net.verts[net.edges[i+1]];
fv = scale(fv);
tv = scale(tv);
ctx.moveTo(fv.x, fv.y);
ctx.lineTo(tv.x, tv.y);
}
ctx.stroke();
}
// A tile's signature is a bitmask telling
// which sides of the tile are walls
function getSignature(tile) {
var neighbours = getOrderedNeighboursTo(tile.x, tile.y);
var sig = 0;
for (var i=0; i<neighbours.length; i++) {
var n = neighbours[i];
if (typeof n === 'undefined') {
continue;
}
if (tiles[n].t === V_TYPE.WALL) {
sig += 1 << i;
}
}
return sig;
}
function countNeighbours(tile) {
var neighbours = getNeighboursTo(tile.x, tile.y);
var count = 0;
for (var i=0; i<neighbours.length; i++) {
var n = neighbours[i];
count += clamp01(tiles[n].t);
}
return count;
}
function getNeighboursBySig(tile, sig) {
var neighbours = [];
var dirs = [{x:0,y:1},{x:-1,y:0},{x:0,y:-1},{x:1,y:0}];
for (var i=1, n=0; i<16; i*=2, n++) {
if (sig & i === 0) {
continue;
}
var x = tile.x + dirs[n].x;
var y = tile.y + dirs[n].y;
var j = flattenIndex(x, y);
if (j < 0 || j >= tiles.length) {
continue;
}
neighbours[n] = tiles[j];
}
return neighbours;
}
// Returns a list of neighbours within the
// Von Neumann neighbourhood. The list is
// not ordered on neighbourhood
function getNeighboursTo(x, y) {
var addNeighbour = function(x, y, list) {
var i = flattenIndex(x, y);
if (i >= 0 && i < tiles.length) {
list.push(i);
}
}
var neighbours = [];
addNeighbour(x, y+1, neighbours); // S
addNeighbour(x-1, y , neighbours); // W
addNeighbour(x, y-1, neighbours); // N
addNeighbour(x+1, y , neighbours); // E
return neighbours;
}
function getOrderedNeighboursTo(x, y) {
var getNeighbour = function(x, y) {
var i = flattenIndex(x, y);
if (i >= 0 && i < tiles.length) {
return i;
}
return -1;
}
var neighbours = [];
neighbours.push(getNeighbour(x, y+1)); // S
neighbours.push(getNeighbour(x-1, y )); // W
neighbours.push(getNeighbour(x, y-1)); // N
neighbours.push(getNeighbour(x+1, y )); // E
return neighbours;
}
function getOrderedMooreNeighboursTo(x, y) {
var getNeighbour = function(x, y) {
var i = flattenIndex(x, y);
if (i >= 0 && i < tiles.length) {
return i;
}
return -1;
}
var neighbours = [];
neighbours.push(getNeighbour(x, y+1)); // S
neighbours.push(getNeighbour(x-1, y+1)); // SW
neighbours.push(getNeighbour(x-1, y )); // W
neighbours.push(getNeighbour(x-1, y-1)); // NW
neighbours.push(getNeighbour(x, y-1)); // N
neighbours.push(getNeighbour(x+1, y-1)); // NE
neighbours.push(getNeighbour(x+1, y )); // E
neighbours.push(getNeighbour(x+1, y+1)); // SE
return neighbours;
}
function eachNeighbour(tile, cb) {
var neighbours = getNeighboursTo(tile.x, tile.y);
for (var i=0; i<neighbours.length; i++) {
var n = neighbours[i];
cb(tiles[n], i, n);
}
}
function processJobs(jobs, cb) {
var numJobs = jobs.length;
var counter = 0;
while (counter < numJobs) {
cb(jobs.shift());
counter++;
}
}
function growFrom(tile) {
jobsGrow.push(tile);
window.setTimeout(grow, cfg.jobSpeed);
}
function grow() {
processJobs(jobsGrow, function(tile) {
var neighbours = getNeighboursTo(tile.x, tile.y);
for (var i=0; i<neighbours.length; i++) {
var n = neighbours[i];
// We don't care about neighbours that are greater
// or equal, or exactly one value lower than the
// current tile.
var diff = tiles[n].v - tile.v;
if (diff >= 0 || diff === -1) {
continue;
}
// Ignore walls
if (tiles[n].t === V_TYPE.WALL) {
continue;
}
tiles[n].v = tile.v - 1;
if (tiles[n].v > 1) {
jobsGrow.push(tiles[n]);
}
}
});
render();
if (jobsGrow.length > 0) {
window.setTimeout(grow, cfg.jobSpeed);
}
}
function shrinkFrom(tile) {
var addJob = function(tile) {
var neighbours = getNeighboursTo(tile.x, tile.y);
for (var i=0; i<neighbours.length; i++) {
jobsShrink.push(tiles[neighbours[i]]);
}
jobsShrink.push(tile);
}
if (tile instanceof Array) {
for (var t=0; t<tile.length; t++) {
addJob(tile[t]);
}
} else {
addJob(tile);
}
window.setTimeout(shrink, cfg.jobSpeed);
}
function shrink() {
var moreJobs = [];
processJobs(jobsShrink, function(tile) {
var neighbours = getNeighboursTo(tile.x, tile.y);
var v = {v:0};
for (var i=0; i<neighbours.length; i++) {
var ni = neighbours[i];
if (tiles[ni].v > v.v) {
v = tiles[ni];
}
}
if (v.v <= tile.v) {
var newVal = v.v - 1;
if (newVal >= 0) {
tile.v = newVal;
moreJobs.push(tile);
} else {
tile.v = 0;
}
}
});
render();
shrinkFrom(moreJobs);
}
canvas.addEventListener('click', function(event) {
var click = getTile(event, cfg.cellSize);
var i = flattenIndex(click.x, click.y);
var tile = tiles[i];
tools[curTool].callback(tile);
render();
});
function registerTools(tools, defaultTool) {
var container = document.getElementById('tools');
Object.keys(tools).forEach(function(tool) {
var a = document.createElement('a');
a.innerHTML= tool;
a.dataset.tool = tool;
container.appendChild(a);
tools[tool].element = a;
});
container.addEventListener('click', function(event) {
setTool(event.target.dataset.tool);
});
setTool(defaultTool);
}
function setTool(tool) {
curTool = tool;
Object.keys(tools).forEach(function(tool) {
var elm = tools[tool].element;
elm.id = elm.dataset.tool == curTool ? 'selected' : '';
});
}
function log(str) {
logs.push(str);
var diff = Math.max(logs.length - 20, 0);
logs.splice(0, diff);
while (logContainer.firstChild) {
logContainer.removeChild(logContainer.firstChild);
}
for (var i=0; i<logs.length; i++) {
var l = document.createElement('span');
l.innerHTML = logs[i];
logContainer.appendChild(l);
}
}
function stat(val) {
statsContainer.innerHTML = val;
}
function clamp01(val) {
return clamp(val, 0, 1);
}
function clamp(val, min, max) {
return Math.min(Math.max(val, min), max);
}
function map(val, mina, maxa, minb, maxb) {
return minb + (maxb - minb) * ((val - mina) / (maxa - mina));
}
canvas.width = cfg.cellSize * cfg.gridWidth;
canvas.height = cfg.cellSize * cfg.gridHeight;
fillTiles();
/*
drawRect(4, 4, 8, 8, 3);
drawRect(11, 4, 5, 5, 3);
drawRect(4, 11, 8, 8, 3);
drawRect(11, 11, 12, 12, 3);
drawDot(6, 11, 0);
//drawDot(11, 14, 0);
floodFill(5, 5, V_TYPE.ROOM);
*/
render();
registerTools(tools, 'room');
logContainer = document.getElementById('log');
statsContainer = document.getElementById('stats');
</script></body>
</html>
body {
margin: 50px 25px 0 25px;
background-color: #ccc;
}
canvas {
float: left;
}
#stats {
float: left;
}
#tools {
clear: left;
padding: 0.1rem;
}
#tools a {
margin: 0 0.3rem 0 0;
font-size: 0.9rem;
font-family: sans-serif;
}
#tools a#selected {
background-color: red;
color: white;
}
#log {
background-color: white;
margin-top: 0.5rem;
}
#log span {
display: block;
font-family: monospace;
}
var cfg = {
cellSize: 9,
tileColors: [
function(v) { return 'rgb(255,255,255)'; },
function(v) {
var fv = Math.floor(map(v, 0, 1, 0, 225));
return 'rgb('+fv+','+fv+','+fv+')'; },
function(v) { return 'rgb(0,255,0)'; },
function(v) { return 'rgb(0,0,0)'; },
function(v) { return 'rgb(127,0,127)'; },
],
jobSpeed: 250,
gridWidth: 32,
gridHeight: 32,
gridAreal: 32 * 32
};
var V_TYPE = {
VACUUM: 0,
ROOM: 1,
PRODUCER: 2,
WALL: 3,
REPAIR: 4,
};
var logs = [];
var logContainer;
var statsContainer;
var jobsGrow = [];
var jobsShrink = [];
var jobsRepair = [];
var tiles = [];
var curTool;
var canvas = document.getElementById('c');
var ctx = canvas.getContext('2d');
var offsetsDiag = [
{x:-1, y:1}, {x:-1, y:-1}, {x:1, y:-1}, {x:1, y:1}
];
var offsetsVonN = [
{x:0, y:1}, {x:-1, y:0}, {x:0, y:-1}, {x:1, y:0}
];
var offsetsMoore = [
{x:0, y:1}, {x:-1, y:1}, {x:-1, y:0},
{x:-1, y:-1}, {x:0, y:-1}, {x:1, y:-1},
{x:1, y:0}, {x:1, y:1}
];
var tools = {
vacuum: {
callback: function(tile) {
if (tile.t === V_TYPE.PRODUCER ||
tile.t === V_TYPE.ROOM ||
tile.t === V_TYPE.VACUUM) {
return;
}
addVacuum(tile);
}
},
room: {
callback: function(tile) {
if (tile.t === V_TYPE.PRODUCER) {
return;
}
var neighbours = getOrderedNeighboursTo(tile.x, tile.y);
for (var j=0; j<neighbours.length; j++) {
var ni = neighbours[j];
if (ni === -1) {
continue;
}
if (tiles[ni].t !== V_TYPE.VACUUM) {
continue;
}
tiles[ni].t = V_TYPE.WALL;
}
// If any of the neighbours are vacuum, consume
// the tile by vacuum.
/*
var vacuum = false;
eachNeighbour(tile, function(ntile, n, i) {
if (ntile.t === V_TYPE.VACUUM) {
vacuum = true;
}
});
if (vacuum) {
tile.t = V_TYPE.ROOM;
fillVacuum(tile);
return;
}
*/
addRoom(tile);
// Otherwise, add room and continue the
// grow algorithm if any
var ns = getNeighboursTo(tile.x, tile.y);
var max = 0;
var m = -1;
for (var i=0; i<ns.length; i++) {
var n = ns[i];
if (tiles[n].v > max) {
max = tiles[n].v;
m = i;
}
}
if (max > 0) {
growFrom(tiles[ns[m]]);
}
}
},
producer: {
callback: function(tile) {
if (tile.t === V_TYPE.PRODUCER) {
tile.t = V_TYPE.ROOM;
tile.v = 7;
shrinkFrom(tile);
return
}
if (tile.t === V_TYPE.ROOM ||
tile.t === V_TYPE.VACUUM) {
tile.t = V_TYPE.PRODUCER;
tile.v = 8;
growFrom(tile);
return;
}
}
},
wall: {
callback: function(tile) {
if (tile.t === V_TYPE.PRODUCER) {
return;
}
// When repairing a wall
if (tile.t === V_TYPE.VACUUM) {
// If tile has two adjacent neighbours, either NS or EW,
// consider it a repair
var sig = getSignature(tile);
if (sig === 5 || sig === 10) {
console.log('repair');
var neighs = getNeighboursTo(tile.x, tile.y);
for (var i=0; i<neighs.length; i++) {
var n = neighs[i];
var nt = tiles[n];
var v = net.findVertex(nt.x, nt.y);
console.log(nt.x, nt.y, 'has vertex', v);
if (v < 0) {
continue;
}
console.log('fill from', nt.x, nt.y, '(',tile.x, tile.y,')');
floodFill(nt.x, nt.y, V_TYPE.ROOM);
net.addEdgesToTile(nt);
net.addFacesToTile(nt);
updateStats();
// look for neighbours that are of type ROOM
// if (tiles[n].t !== V_TYPE.ROOM) {
// continue;
// }
// var ok = repairFill(nt.x, nt.y);
// log("ok "+ok);
// if (ok) {
tile.t = V_TYPE.WALL;
tile.v = 0;
// }
}
return;
}
}
tile.t = V_TYPE.WALL;
tile.v = 0;
var min = 8;
var ni = -1;
eachNeighbour(tile, function(n, i) {
if (n.t !== V_TYPE.ROOM &&
n.t !== V_TYPE.PRODUCER) {
return;
}
if (n.t === V_TYPE.ROOM) {
net.addEdgesToTile(n);
net.addFacesToTile(n);
updateStats();
}
if (n.v < min) {
min = n.v;
ni = flattenIndex(n.x, n.y);
}
});
if (min === 0 || min === 8) {
return;
}
var top = crawlUp(tiles[ni]);
if (top.v < 8) {
shrinkFrom(top);
}
}
},
};
var net = {
verts: [],
edges: [],
faces: [],
// bitmasks for illegal combos. There are only three (or two
// in this case):
// - when there are exactly three neighbours, and they are all
// on line, either along the x or y axis. So something like:
// 1010 and 0101
illegals: [],
getNeighbours: function(x, y) {
var neighbours = [];
neighbours.push(this.findVertex(x, y+1)); // S
neighbours.push(this.findVertex(x-1, y )); // W
neighbours.push(this.findVertex(x , y-1)); // N
neighbours.push(this.findVertex(x+1, y )); // E
return neighbours;
},
addMooreNeighbours: function(list, tile, offsets) {
for (var n=0, o=1; n<offsets.length; n++, o+=2) {
var v = this.findVertex(tile.x + offsets[n].x,
tile.y + offsets[n].y);
list.splice(o, 0, v);
}
},
addVertex: function(tile) {
this.verts.push({x: tile.x, y: tile.y});
var v = this.verts.length - 1;
var neighbours = this.getNeighbours(tile.x, tile.y);
this.addEdges(v, neighbours);
this.addMooreNeighbours(neighbours, tile, offsetsDiag);
this.addFaces(v, neighbours);
},
findVertex: function(x, y) {
for (var i=0; i<this.verts.length; i++) {
if (this.verts[i].x === x &&
this.verts[i].y === y) {
return i;
}
}
return -1;
},
addEdgesToTile: function(tile) {
var v = this.findVertex(tile.x, tile.y);
var neighbours = this.getNeighbours(tile.x, tile.y);
this.addEdges(v, neighbours);
},
addEdges: function(fromVert, neighbours) {
function valid(n) {
return n > -1;
}
var nlist = neighbours.filter(valid);
for (var i=0; i<nlist.length; i++) {
this.edges.push(fromVert);
this.edges.push(nlist[i]);
}
},
findEdges: function(v) {
var edges = [];
for (var i=0; i<this.edges.length; i+=2) {
if (this.edges[i] === v ||
this.edges[i+1] === v) {
edges.push(i);
}
}
return edges;
},
removeEdges: function(tile) {
var v = this.findVertex(tile.x, tile.y);
var edges = this.findEdges(v);
for (var i=edges.length-1; i>=0; i--) {
this.edges.splice(edges[i], 2);
}
},
addFacesToTile: function(tile) {
var v = this.findVertex(tile.x, tile.y);
var neighbours = this.getNeighbours(tile.x, tile.y);
this.addMooreNeighbours(neighbours, tile, offsetsDiag);
this.addFaces(v, neighbours);
},
addFaces: function(fromVert, neighbours) {
var exists = function(a, b, c) {
return this[a] > -1 &&
this[b] > -1 &&
this[c] > -1;
}.bind(neighbours);
var create = function(a, b, c) {
if (exists(a, b, c)) {
this.faces.push(fromVert);
this.faces.push(neighbours[a]);
this.faces.push(neighbours[b]);
this.faces.push(neighbours[c]);
}
}.bind(this);
create(0,1,2);
create(2,3,4);
create(4,5,6);
create(6,7,0);
},
findFaces: function(v) {
var faces = [];
for (var i=0; i<this.faces.length; i+=4) {
if (this.faces[i] === v ||
this.faces[i+1] === v ||
this.faces[i+2] === v ||
this.faces[i+3] === v) {
faces.push(i);
}
}
return faces;
},
removeFaces: function(tile) {
var v = this.findVertex(tile.x, tile.y);
var faces = this.findFaces(v);
for (var i=faces.length-1; i>=0; i--) {
this.faces.splice(faces[i], 4);
}
},
};
function addVacuum(tile) {
if (tile.t === V_TYPE.WALL) {
eachNeighbour(tile, function(n, i) {
if (n.t === V_TYPE.ROOM) {
net.removeEdges(n);
net.removeFaces(n);
updateStats();
}
});
}
eachNeighbour(tile, function(n, i) {
if (n.t === V_TYPE.ROOM) {
fillVacuum(n);
}
});
tile.t = V_TYPE.VACUUM;
}
function addRoom(tile) {
tile.t = V_TYPE.ROOM;
net.addVertex(tile);
updateStats();
// network.addVertex(tile.x, tile.y)
// - check neighbours (look in vertices list)
// - add edges and faces
// num neighbours
// 0: add its own vertex to network mesh
// 1: add vertex, 1 edge
// 2: add vertex, 2 edges, 1 face
// 3: add vertex, 3 edges, 2 faces
// 4: add vertex, 4 edges, 3 faces
// edges = neighbours
// faces = edges - 1
}
function updateStats() {
var v = net.verts.length;
var e = net.edges.length / 2;
var f = net.faces.length / 4;
var state = v - e + f;
stat((state === 1 ? 'closed' : 'open') + ' ('+v+', '+e+', '+f+') '+state);
}
function drawRect(x, y, w, h, t) {
drawLine([x, y], w, 0, t);
drawLine([x, y+1], h-2, 1, t);
drawLine([x+w-1, y+1], h-2, 1, t);
drawLine([x, y+h-1], w, 0, t);
}
// dir is either 0 or 1, depending on the direction
// you want to draw. 0 = x and 1 = y
function drawLine(coord, len, dir, t) {
for (var i=0; i<len; i++) {
var n = flattenIndex(coord[0], coord[1]);
tiles[n].t = t;
coord[dir]++;
}
}
function drawDot(x, y, t) {
var n = flattenIndex(x, y);
tiles[n].t = t;
}
function floodFill(x, y, t, cb) {
var fi = flattenIndex(x, y);
var fill_type = tiles[fi].t;
var fill = [];
fill.push(tiles[fi]);
while (fill.length > 0) {
var tile = fill.shift();
if (tile.t === t) {
continue;
}
tile.t = t;
if (typeof cb !== 'undefined') {
cb(tile);
}
var neighbours = getNeighboursTo(tile.x, tile.y);
for (var i=0; i<neighbours.length; i++) {
var n = neighbours[i];
if (tiles[n].t === fill_type) {
fill.push(tiles[n]);
}
}
}
}
function repairFill(x, y) {
var fi = flattenIndex(x, y);
var fill = [];
fill.push(tiles[fi]);
var success = true;
while (fill.length > 0) {
var tile = fill.shift();
if (tile.t === V_TYPE.VACUUM) {
success = false;
break;
}
tile.t = V_TYPE.REPAIR;
var neighbours = getNeighboursTo(tile.x, tile.y);
for (var i=0; i<neighbours.length; i++) {
var n = neighbours[i];
if (tiles[n].t === V_TYPE.ROOM) {
fill.push(tiles[n]);
}
}
}
return success;
}
function crawlUp(tile) {
var ti = flattenIndex(tile.x, tile.y);
var list = [];
list.push(tiles[ti]);
var top = 0;
while (list.length > 0) {
var t = list.shift();
if (t.v === 0 || t.v === 8) {
break;
}
var max = 0;
var ni = -1;
var neighbours = getNeighboursTo(t.x, t.y);
for (var i=0; i<neighbours.length; i++) {
var n = neighbours[i];
if (tiles[n].v > max) {
max = tiles[n].v;
ni = n;
}
}
if (max > 0) {
top = max;
ti = ni;
list.push(tiles[ni]);
}
}
return tiles[ti];
}
function fillVacuum(tile) {
floodFill(tile.x,
tile.y,
V_TYPE.VACUUM,
function(t) { t.a = -1; });
}
function flattenIndex(x, y) {
return x + cfg.gridWidth * y;
}
function setTiles(callback) {
var x=0;
var y=0;
for (var i=0; i<cfg.gridAreal; i++) {
x = (x + 1) % cfg.gridWidth;
y = Math.floor(i / cfg.gridHeight);
var n = flattenIndex(x,y);
tiles[n] = callback(tiles[n], x, y, i);
}
}
function fillTiles() {
setTiles(function(tile, x, y, i) {
return {x:x, y:y, t:V_TYPE.VACUUM, v:0, a:0};
});
}
function clear() {
ctx.fillStyle = 'rgb(255,255,255)';
ctx.fillRect(0, 0,
cfg.cellSize * cfg.gridWidth,
cfg.cellSize * cfg.gridHeight);
}
function getTile(event, cellSize) {
return {
x: Math.floor(event.offsetX / cellSize),
y: Math.floor(event.offsetY / cellSize)
};
}
function render() {
clear();
var scale = function(val) {
return {
x: val.x * cfg.cellSize + cfg.cellSize * 0.5,
y: val.y * cfg.cellSize + cfg.cellSize * 0.5,
}
}
var i;
for (i=0; i<cfg.gridAreal; i++) {
var tile = tiles[i];
var color = cfg.tileColors[tile.t];
ctx.fillStyle = color(1 - (tile.v / 8));
ctx.fillRect(tile.x * cfg.cellSize,
tile.y * cfg.cellSize,
cfg.cellSize,
cfg.cellSize);
}
for (i=0; i<net.verts.length; i++) {
var vert = net.verts[i];
ctx.fillStyle = 'rgb(0, 0, 0)';
ctx.fillRect(scale(vert.x), scale(vert.y), 1, 1);
}
ctx.beginPath();
for (i=0; i<net.edges.length; i+=2) {
var fv = net.verts[net.edges[i]];
var tv = net.verts[net.edges[i+1]];
fv = scale(fv);
tv = scale(tv);
ctx.moveTo(fv.x, fv.y);
ctx.lineTo(tv.x, tv.y);
}
ctx.stroke();
}
// A tile's signature is a bitmask telling
// which sides of the tile are walls
function getSignature(tile) {
var neighbours = getOrderedNeighboursTo(tile.x, tile.y);
var sig = 0;
for (var i=0; i<neighbours.length; i++) {
var n = neighbours[i];
if (typeof n === 'undefined') {
continue;
}
if (tiles[n].t === V_TYPE.WALL) {
sig += 1 << i;
}
}
return sig;
}
function countNeighbours(tile) {
var neighbours = getNeighboursTo(tile.x, tile.y);
var count = 0;
for (var i=0; i<neighbours.length; i++) {
var n = neighbours[i];
count += clamp01(tiles[n].t);
}
return count;
}
function getNeighboursBySig(tile, sig) {
var neighbours = [];
var dirs = [{x:0,y:1},{x:-1,y:0},{x:0,y:-1},{x:1,y:0}];
for (var i=1, n=0; i<16; i*=2, n++) {
if (sig & i === 0) {
continue;
}
var x = tile.x + dirs[n].x;
var y = tile.y + dirs[n].y;
var j = flattenIndex(x, y);
if (j < 0 || j >= tiles.length) {
continue;
}
neighbours[n] = tiles[j];
}
return neighbours;
}
// Returns a list of neighbours within the
// Von Neumann neighbourhood. The list is
// not ordered on neighbourhood
function getNeighboursTo(x, y) {
var addNeighbour = function(x, y, list) {
var i = flattenIndex(x, y);
if (i >= 0 && i < tiles.length) {
list.push(i);
}
}
var neighbours = [];
addNeighbour(x, y+1, neighbours); // S
addNeighbour(x-1, y , neighbours); // W
addNeighbour(x, y-1, neighbours); // N
addNeighbour(x+1, y , neighbours); // E
return neighbours;
}
function getOrderedNeighboursTo(x, y) {
var getNeighbour = function(x, y) {
var i = flattenIndex(x, y);
if (i >= 0 && i < tiles.length) {
return i;
}
return -1;
}
var neighbours = [];
neighbours.push(getNeighbour(x, y+1)); // S
neighbours.push(getNeighbour(x-1, y )); // W
neighbours.push(getNeighbour(x, y-1)); // N
neighbours.push(getNeighbour(x+1, y )); // E
return neighbours;
}
function getOrderedMooreNeighboursTo(x, y) {
var getNeighbour = function(x, y) {
var i = flattenIndex(x, y);
if (i >= 0 && i < tiles.length) {
return i;
}
return -1;
}
var neighbours = [];
neighbours.push(getNeighbour(x, y+1)); // S
neighbours.push(getNeighbour(x-1, y+1)); // SW
neighbours.push(getNeighbour(x-1, y )); // W
neighbours.push(getNeighbour(x-1, y-1)); // NW
neighbours.push(getNeighbour(x, y-1)); // N
neighbours.push(getNeighbour(x+1, y-1)); // NE
neighbours.push(getNeighbour(x+1, y )); // E
neighbours.push(getNeighbour(x+1, y+1)); // SE
return neighbours;
}
function eachNeighbour(tile, cb) {
var neighbours = getNeighboursTo(tile.x, tile.y);
for (var i=0; i<neighbours.length; i++) {
var n = neighbours[i];
cb(tiles[n], i, n);
}
}
function processJobs(jobs, cb) {
var numJobs = jobs.length;
var counter = 0;
while (counter < numJobs) {
cb(jobs.shift());
counter++;
}
}
function growFrom(tile) {
jobsGrow.push(tile);
window.setTimeout(grow, cfg.jobSpeed);
}
function grow() {
processJobs(jobsGrow, function(tile) {
var neighbours = getNeighboursTo(tile.x, tile.y);
for (var i=0; i<neighbours.length; i++) {
var n = neighbours[i];
// We don't care about neighbours that are greater
// or equal, or exactly one value lower than the
// current tile.
var diff = tiles[n].v - tile.v;
if (diff >= 0 || diff === -1) {
continue;
}
// Ignore walls
if (tiles[n].t === V_TYPE.WALL) {
continue;
}
tiles[n].v = tile.v - 1;
if (tiles[n].v > 1) {
jobsGrow.push(tiles[n]);
}
}
});
render();
if (jobsGrow.length > 0) {
window.setTimeout(grow, cfg.jobSpeed);
}
}
function shrinkFrom(tile) {
var addJob = function(tile) {
var neighbours = getNeighboursTo(tile.x, tile.y);
for (var i=0; i<neighbours.length; i++) {
jobsShrink.push(tiles[neighbours[i]]);
}
jobsShrink.push(tile);
}
if (tile instanceof Array) {
for (var t=0; t<tile.length; t++) {
addJob(tile[t]);
}
} else {
addJob(tile);
}
window.setTimeout(shrink, cfg.jobSpeed);
}
function shrink() {
var moreJobs = [];
processJobs(jobsShrink, function(tile) {
var neighbours = getNeighboursTo(tile.x, tile.y);
var v = {v:0};
for (var i=0; i<neighbours.length; i++) {
var ni = neighbours[i];
if (tiles[ni].v > v.v) {
v = tiles[ni];
}
}
if (v.v <= tile.v) {
var newVal = v.v - 1;
if (newVal >= 0) {
tile.v = newVal;
moreJobs.push(tile);
} else {
tile.v = 0;
}
}
});
render();
shrinkFrom(moreJobs);
}
canvas.addEventListener('click', function(event) {
var click = getTile(event, cfg.cellSize);
var i = flattenIndex(click.x, click.y);
var tile = tiles[i];
tools[curTool].callback(tile);
render();
});
function registerTools(tools, defaultTool) {
var container = document.getElementById('tools');
Object.keys(tools).forEach(function(tool) {
var a = document.createElement('a');
a.innerHTML= tool;
a.dataset.tool = tool;
container.appendChild(a);
tools[tool].element = a;
});
container.addEventListener('click', function(event) {
setTool(event.target.dataset.tool);
});
setTool(defaultTool);
}
function setTool(tool) {
curTool = tool;
Object.keys(tools).forEach(function(tool) {
var elm = tools[tool].element;
elm.id = elm.dataset.tool == curTool ? 'selected' : '';
});
}
function log(str) {
logs.push(str);
var diff = Math.max(logs.length - 20, 0);
logs.splice(0, diff);
while (logContainer.firstChild) {
logContainer.removeChild(logContainer.firstChild);
}
for (var i=0; i<logs.length; i++) {
var l = document.createElement('span');
l.innerHTML = logs[i];
logContainer.appendChild(l);
}
}
function stat(val) {
statsContainer.innerHTML = val;
}
function clamp01(val) {
return clamp(val, 0, 1);
}
function clamp(val, min, max) {
return Math.min(Math.max(val, min), max);
}
function map(val, mina, maxa, minb, maxb) {
return minb + (maxb - minb) * ((val - mina) / (maxa - mina));
}
canvas.width = cfg.cellSize * cfg.gridWidth;
canvas.height = cfg.cellSize * cfg.gridHeight;
fillTiles();
/*
drawRect(4, 4, 8, 8, 3);
drawRect(11, 4, 5, 5, 3);
drawRect(4, 11, 8, 8, 3);
drawRect(11, 11, 12, 12, 3);
drawDot(6, 11, 0);
//drawDot(11, 14, 0);
floodFill(5, 5, V_TYPE.ROOM);
*/
render();
registerTools(tools, 'room');
logContainer = document.getElementById('log');
statsContainer = document.getElementById('stats');
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment