581 lines
13 KiB
JavaScript
Executable File
581 lines
13 KiB
JavaScript
Executable File
/**
|
|
Interactive cellular automata.
|
|
|
|
Michael Soukup 2014.
|
|
*/
|
|
|
|
|
|
function zeros(m, n) {
|
|
var mat = [];
|
|
for (var i=0; i<m; i++) {
|
|
mat.push([]);
|
|
for (var j=0; j<n; j++) {
|
|
mat[i].push(0);
|
|
}
|
|
}
|
|
return mat
|
|
}
|
|
|
|
function nearestMod0(f, n) {
|
|
var mod = f % n;
|
|
if (mod < n/2) { // floor
|
|
return f-mod
|
|
}
|
|
else { // ceil
|
|
return f+n-mod
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
var CELL_T = {
|
|
'empty': 0,
|
|
'wall': 1,
|
|
'player': 2,
|
|
'enemy': 3,
|
|
'conway': 4
|
|
};
|
|
|
|
var CELLCOL = {
|
|
0: '#fff',
|
|
1: '#222',
|
|
2: '#484',
|
|
3: '#a44',
|
|
4: '#8a8a8a'
|
|
};
|
|
|
|
// Note: Cellsets appear transposed.
|
|
var CELLSETS = {
|
|
"dead_4": [
|
|
[4,4],
|
|
[4,4]
|
|
],
|
|
"dead_9": [
|
|
[0,4,0],
|
|
[4,0,4],
|
|
[0,4,0]
|
|
],
|
|
"finite14_64": [
|
|
[0,0,0,0,0,0,0,0],
|
|
[0,4,0,4,4,4,0,0],
|
|
[0,0,0,0,4,4,0,0],
|
|
[0,0,4,4,0,4,0,0],
|
|
[0,0,4,4,0,0,0,0],
|
|
[0,0,0,4,4,4,0,0],
|
|
[0,4,4,0,4,4,0,0],
|
|
[0,4,4,0,0,0,0,0]
|
|
],
|
|
"infinite_64": [
|
|
[0,0,0,4,0,0,0,0],
|
|
[0,4,4,4,4,4,4,0],
|
|
[4,0,0,0,4,4,0,0],
|
|
[0,4,4,4,0,4,0,0],
|
|
[0,4,0,0,4,4,0,4],
|
|
[0,0,0,4,0,0,0,0],
|
|
[0,4,0,4,0,4,0,4],
|
|
[0,4,4,4,0,0,0,0]
|
|
],
|
|
"player_0": [
|
|
[2,2,2,2],
|
|
[2,0,4,2],
|
|
[2,0,0,2],
|
|
[2,2,2,2]
|
|
],
|
|
"enemy_0": [
|
|
[3,3,3,3],
|
|
[3,0,0,3],
|
|
[3,0,0,3],
|
|
[3,3,3,3]
|
|
]
|
|
}
|
|
|
|
var WORLD_0 = {
|
|
"width": 80, // tilewidth
|
|
"heigth": 60, // tileheight
|
|
"walls": true, // sets boundaries if true
|
|
"player": ["player_0", 20, 20], // tileset, x, y
|
|
"enemies": [
|
|
["enemy_0", 50, 40]
|
|
],
|
|
"organisms": [
|
|
["finite14_64", 25, 25],
|
|
["infinite_64", 65, 40],
|
|
["dead_4", 10, 5],
|
|
["dead_4", 5, 10],
|
|
["dead_4", 30, 5],
|
|
["dead_9", 17, 40],
|
|
]
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
Update rules
|
|
|
|
c - cell identifier
|
|
X - neighbourhood of c. Array of length 8 starting in upper left corner.
|
|
*/
|
|
|
|
function _ndist(X) {
|
|
var dist = [0,0,0,0,0];
|
|
for (var i=0; i<X.length; i++) {
|
|
dist[X[i]]++;
|
|
}
|
|
return dist
|
|
}
|
|
|
|
function cell_0_update(X) {
|
|
// count neighbours for now
|
|
var dist = _ndist(X);
|
|
if (dist[4] == 3) {
|
|
return 4
|
|
}
|
|
else {
|
|
return 0
|
|
}
|
|
}
|
|
|
|
function cell_1_update(X) {
|
|
return 1
|
|
}
|
|
|
|
function cell_2_update(X) {
|
|
// Player cell
|
|
var dist = _ndist(X);
|
|
if (dist[2] < 1 || dist[2] > 7) {
|
|
return 0;
|
|
}
|
|
else if (dist[2] == 1 && dist[4] < 3) {
|
|
return 0;
|
|
}
|
|
else {
|
|
return 2;
|
|
}
|
|
}
|
|
|
|
function cell_3_update(X) {
|
|
// enemy cell
|
|
return 3
|
|
}
|
|
|
|
function cell_4_update(X) {
|
|
// conway live cells
|
|
var dist = _ndist(X);
|
|
if (dist[2] == 3 && dist[4] > 2) {
|
|
return 2
|
|
}
|
|
else if (dist[2] > 0 && (dist[4] != 5 || dist[4] == 4)) {
|
|
return 4
|
|
}
|
|
else if (dist[4] == 2 || dist[4] == 3) {
|
|
return 4
|
|
}
|
|
else {
|
|
return 0
|
|
}
|
|
}
|
|
|
|
function cell_next(c, X) {
|
|
|
|
switch (c) {
|
|
case 0:
|
|
return cell_0_update(X);
|
|
case 1:
|
|
return cell_1_update(X);
|
|
case 2:
|
|
return cell_2_update(X);
|
|
case 3:
|
|
return cell_3_update(X);
|
|
case 4:
|
|
return cell_4_update(X);
|
|
case 5:
|
|
return cell_0_update(X);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
Board holds the world variables and a complete map over all tiles.
|
|
All tiles on the board are simlulated in each step.
|
|
*/
|
|
function Board() {
|
|
this.x = 0;
|
|
this.y = 0;
|
|
}
|
|
|
|
Board.prototype.on_board = function(x, y) {
|
|
// TODO check walls
|
|
return (x >= this.x && y >= this.y && x < this.x+this.w && y < this.y+this.h);
|
|
}
|
|
|
|
Board.prototype.set_tile = function(val, x, y) {
|
|
if (this.on_board(x, y)) {
|
|
this.map[x][y] = val;
|
|
}
|
|
}
|
|
|
|
Board.prototype.get_tile = function(x, y) {
|
|
if (this.on_board(x, y)) {
|
|
return this.map[x][y];
|
|
}
|
|
else {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
Board.prototype.load_tileset = function(tileset, x, y) {
|
|
for (var i=0; i<tileset[0].length; i++) {
|
|
for (var j=0; j<tileset.length; j++) {
|
|
this.set_tile(tileset[j][i], x+i, y+j);
|
|
}
|
|
}
|
|
}
|
|
|
|
Board.prototype.load_world = function(world) {
|
|
this.w = world.width;
|
|
this.h = world.heigth;
|
|
this.map = zeros(this.w, this.h);
|
|
this.map_buf = zeros(this.w, this.h); // Buffer for simulation
|
|
|
|
this.load_tileset(CELLSETS[world.player[0]], world.player[1], world.player[2]);
|
|
for (var e=0; e<world.enemies.length; e++) {
|
|
var _enemy = world.enemies[e];
|
|
this.load_tileset(CELLSETS[_enemy[0]], _enemy[1], _enemy[2]);
|
|
}
|
|
for (var o=0; o<world.organisms.length; o++) {
|
|
var _organism = world.organisms[o];
|
|
this.load_tileset(CELLSETS[_organism[0]], _organism[1], _organism[2]);
|
|
}
|
|
|
|
if (world.walls) {
|
|
for (var i=this.x; i<this.x+this.w; i++) {
|
|
this.set_tile(1, i, 0);
|
|
this.set_tile(1, i, this.h-1);
|
|
}
|
|
for (var j=this.y; j<this.y+this.h; j++) {
|
|
this.set_tile(1, 0, j);
|
|
this.set_tile(1, this.w-1, j);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
Move the set of tiles defined by frame to (x+dx, y+dy).
|
|
Not responsible for checking if the move is a legal operation.
|
|
*/
|
|
Board.prototype.move_frame = function(frame, dx, dy) {
|
|
var x_start, x_end, x_step,
|
|
y_start, y_end, y_step,
|
|
x, y;
|
|
|
|
if (dx < 0) {
|
|
x_start = frame.x;
|
|
x_end = frame.x+frame.w;
|
|
x_step = 1;
|
|
}
|
|
else {
|
|
x_start = frame.x+frame.w-1;
|
|
x_end = frame.x-1;
|
|
x_step = -1;
|
|
}
|
|
if (dy < 0) {
|
|
y_start = frame.y;
|
|
y_end = frame.y+frame.h;
|
|
y_step = 1;
|
|
}
|
|
else {
|
|
y_start = frame.y+frame.h-1;
|
|
y_end = frame.y-1;
|
|
y_step = -1;
|
|
}
|
|
|
|
x = x_start;
|
|
while (x != x_end) {
|
|
y = y_start;
|
|
while (y != y_end) {
|
|
this.set_tile(this.map[x][y], x+dx, y+dy);
|
|
this.set_tile(0, x, y);
|
|
y += y_step;
|
|
}
|
|
x += x_step;
|
|
}
|
|
}
|
|
|
|
Board.prototype.neighbourhood = function(cx, cy) {
|
|
return [
|
|
this.get_tile(cx-1, cy-1), // upper left corner
|
|
this.get_tile(cx, cy-1),
|
|
this.get_tile(cx+1, cy-1),
|
|
this.get_tile(cx-1, cy),
|
|
this.get_tile(cx+1, cy),
|
|
this.get_tile(cx-1, cy+1),
|
|
this.get_tile(cx, cy+1),
|
|
this.get_tile(cx+1, cy+1),
|
|
]
|
|
}
|
|
|
|
Board.prototype.simulate = function(frame) {
|
|
var X;
|
|
for (var i=this.x; i<this.x+this.w; i++) {
|
|
for (var j=this.y; j<this.y+this.h; j++) {
|
|
X = this.neighbourhood(i, j);
|
|
this.map_buf[i][j] = cell_next(this.map[i][j], X);
|
|
}
|
|
}
|
|
this.map = this.map_buf;
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
TileFrame defines a square area of tiles with the upper left corner
|
|
in (x, y) and lower right corner in (x+w, y+h).
|
|
|
|
Coordinates in tile space.
|
|
*/
|
|
function TileFrame(x, y, w, h) {
|
|
this.x = x;
|
|
this.y = y;
|
|
this.w = w;
|
|
this.h = h;
|
|
}
|
|
|
|
TileFrame.prototype.move = function(dx, dy) {
|
|
this.x += dx;
|
|
this.y += dy;
|
|
}
|
|
|
|
TileFrame.prototype.contains = function(frame) {
|
|
return (this.x < frame.x && this.y < frame.y && this.x+this.w > frame.x+frame.w && this.y+this.h > frame.y+frame.h);
|
|
}
|
|
|
|
TileFrame.prototype.intersects = function(frame) {
|
|
return (((this.x > frame.x && this.x < frame.x+frame.w) || (frame.x > this.x && frame.x < this.x+this.w)) &&
|
|
((this.y > frame.y && this.y < frame.y+frame.h) || (frame.y > this.y && frame.y < this.y+this.h)));
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
Holds the canvas context and camera coordinates. Implements methods for
|
|
rendering tile sets.
|
|
|
|
Coordinates are continuous and in tile space.
|
|
*/
|
|
function Camera(canvas_id) {
|
|
this.canvas = document.getElementById(canvas_id);
|
|
this.ctx = this.canvas.getContext('2d');
|
|
this.width = this.canvas.width;
|
|
this.height = this.canvas.height;
|
|
|
|
this.tilesize = 5; // Minimum tilesize (px).
|
|
this.tw = this.width/this.tilesize;
|
|
this.th = this.height/this.tilesize;
|
|
}
|
|
|
|
Camera.prototype.zoom = function(tilesize) {
|
|
var factor = tilesize/this.tilesize,
|
|
_tw = this.width/tilesize,
|
|
_th = this.height/tilesize,
|
|
diff_x = (this.tw - _tw)/2,
|
|
diff_y = (this.th - _th)/2;
|
|
|
|
this.x += diff_x;
|
|
this.tw = _tw;
|
|
this.y += diff_y;
|
|
this.th = _th;
|
|
this.tilesize = tilesize;
|
|
}
|
|
|
|
Camera.prototype.center = function(x, y) {
|
|
this.x = x - this.tw/2;
|
|
this.y = y - this.th/2;
|
|
}
|
|
|
|
Camera.prototype.draw_grid = function() {
|
|
var i = (Math.ceil(this.x) - this.x)*this.tilesize,
|
|
j = (Math.ceil(this.y) - this.y)*this.tilesize;
|
|
|
|
this.ctx.strokestyle = 'black';
|
|
this.ctx.lineWidth = 0.1;
|
|
|
|
// Vertical lines
|
|
while (i < this.width) {
|
|
this.ctx.beginPath();
|
|
this.ctx.moveTo(i, 0);
|
|
this.ctx.lineTo(i, this.height);
|
|
this.ctx.closePath();
|
|
this.ctx.stroke();
|
|
i += this.tilesize;
|
|
}
|
|
// Horizontal lines
|
|
while (j < this.height) {
|
|
this.ctx.beginPath();
|
|
this.ctx.moveTo(0, j);
|
|
this.ctx.lineTo(this.width, j);
|
|
this.ctx.closePath();
|
|
this.ctx.stroke();
|
|
j += this.tilesize;
|
|
}
|
|
}
|
|
|
|
Camera.prototype.draw_tiles = function(board) {
|
|
var x, y, i, j, tile_margin = this.tilesize*0.01;
|
|
|
|
x = Math.floor(this.x);
|
|
i = (x - this.x) * this.tilesize;
|
|
while (i < this.width) {
|
|
y = Math.floor(this.y);
|
|
j = (y - this.y)*this.tilesize;
|
|
|
|
while (j < this.height) {
|
|
if (board.get_tile(x,y) != 0) {
|
|
this.ctx.fillStyle = CELLCOL[board.get_tile(x, y)];
|
|
this.ctx.beginPath();
|
|
this.ctx.rect(i+tile_margin, j+tile_margin, this.tilesize-2*tile_margin, this.tilesize-2*tile_margin);
|
|
this.ctx.closePath();
|
|
this.ctx.fill();
|
|
}
|
|
y++;
|
|
j += this.tilesize;
|
|
}
|
|
x++;
|
|
i += this.tilesize;
|
|
}
|
|
}
|
|
|
|
Camera.prototype.clear = function() {
|
|
this.ctx.clearRect(0, 0, this.width, this.height);
|
|
}
|
|
|
|
|
|
|
|
|
|
function testsim(canvas_id, world, dt) {
|
|
var board = new Board(),
|
|
player = new TileFrame(world.player[1], world.player[2], CELLSETS[world.player[0]][0].length, CELLSETS[world.player[0]].length),
|
|
cam = new Camera(canvas_id),
|
|
tilesize = 8;
|
|
|
|
board.load_world(world);
|
|
cam.set(world.player[1]-1.5, world.player[1]-1.5, tilesize); // cam.center(...,
|
|
cam.draw_grid();
|
|
cam.draw_tiles(board);
|
|
setInterval(function (){
|
|
board.simulate();
|
|
cam.clear();
|
|
cam.draw_grid();
|
|
cam.draw_tiles(board);
|
|
}, dt);
|
|
}
|
|
|
|
function testmove(canvas_id, world, dt) {
|
|
var board = new Board(),
|
|
player = new TileFrame(world.player[1], world.player[2], CELLSETS[world.player[0]][0].length, CELLSETS[world.player[0]].length),
|
|
cam = new Camera(canvas_id),
|
|
tilesize = 8.8;
|
|
|
|
board.load_world(world);
|
|
cam.zoom(tilesize);
|
|
cam.center(player.x, player.y);
|
|
cam.draw_grid();
|
|
cam.draw_tiles(board);
|
|
|
|
function expand_player() {
|
|
var X;
|
|
console.log('exp');
|
|
for (var x=player.x; x<player.x+player.w; x++) {
|
|
X = board.neighbourhood(x, player.y);
|
|
console.log(X);
|
|
if (X[0] == 2 || X[1] == 2 || X[2] == 2) {
|
|
player.y--;
|
|
player.h++;
|
|
break;
|
|
}
|
|
X = board.neighbourhood(x, player.y+player.h-1);
|
|
if (X[4] == 2 || X[5] == 2 || X[6] == 2) {
|
|
player.h++;
|
|
break;
|
|
}
|
|
}
|
|
|
|
for (var y=player.y; y<player.y+player.h; y++) {
|
|
X = board.neighbourhood(player.x, y);
|
|
if (X[6] == 2 || X[7] == 2 || X[0] == 2) {
|
|
player.x--;
|
|
player.w++;
|
|
break;
|
|
}
|
|
X = board.neighbourhood(player.x+player.w-1, y);
|
|
if (X[2] == 2 || X[3] == 2 || X[4] == 2) {
|
|
player.w++;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
var i=0;
|
|
addEventListener('keydown', function(e) {
|
|
var vx = vy = 0,
|
|
key = e.keyCode;
|
|
|
|
if (key == 37) {
|
|
vx--;
|
|
}
|
|
else if (key == 39){
|
|
vx++
|
|
}
|
|
else if (key == 40){
|
|
vy++;
|
|
}
|
|
else if (key == 38) {
|
|
vy--;
|
|
}
|
|
else {
|
|
return
|
|
}
|
|
board.move_frame(player, vx, vy);
|
|
player.move(vx, vy);
|
|
board.simulate();
|
|
|
|
cam.center(player.x, player.y);
|
|
cam.clear();
|
|
cam.draw_grid();
|
|
cam.draw_tiles(board);
|
|
|
|
// Expand frames
|
|
expand_player();
|
|
|
|
i++;
|
|
});
|
|
|
|
addEventListener('wheel', function(e) {
|
|
if (e.deltaY > 0) {
|
|
tilesize *= 0.9;
|
|
cam.zoom(tilesize);
|
|
}
|
|
else {
|
|
tilesize *= 1.1;
|
|
cam.zoom(tilesize);
|
|
}
|
|
cam.clear();
|
|
cam.draw_grid();
|
|
cam.draw_tiles(board);
|
|
});
|
|
}
|
|
|
|
|
|
|
|
// Run
|
|
//testsim("canvas", WORLD_0, 1000);
|
|
testmove("canvas", WORLD_0, 1000);
|
|
|
|
|
|
|