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