ca/interactive/ca-interactive.js

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);