Como crear el juego de la vida en JavaScript
El juego de la vida es un autómata celular diseñado por John Conway.
Pero!!! ¿Qué es un autómata celular? Realmente no hay una definición oficial. pero se podria decir que es un sistema que evoluciona con el paso del tiempo asi como la vida.
Pero lo que realmente nos importa es programarlo, así que vamos a darle.
Para construir el juego de la vida necesitaremos tablero que contenga un numero n
de casillas, y pueden ser tantas como queramos., Las casillas deben estar vivas o muertas y siguen 3 simples reglas:
- Si una casilla tiene menos de 2 vecinos, ó más de 3 vecinos vivos, la casilla muere.
- Si tiene exactamente 2 o 3 vecinos vivos se queda igual.
- Si la casilla esta muerta y tiene exactamente 3 vecinos vivos, esta revive.
Con esta introducción ya podemos hacer código.
Primero creamos una carpeta y dentro crearemos 4 archivos.
- index.html
- main.js
- box.js
- board.js
En el index.html
creamos una estructura básica de html, y dentro del body agregamos un canvas con un id único, y luego mandamos a llamar a nuestros archivos JavaScript.
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cellular Automate</title>
</head>
<body>
<canvas id="life-game"></canvas>
<script src="./box.js"></script>
<script src="./board.js"></script>
<script src="./main.js"></script>
</body>
</html>
Pasamos a nuestro archivo box.js
creamos una clase llamada Box
que en su método constructor recibe una posición en x
y en y
, como también un state
. Dentro del constructor inicalizamos sus propiedades, y hacemos bind
de 3 métodos addNeighbors
newCycle
mutation
. Esto es para que el contexto de los métodos no cambie.
class Box {
constructor(x, y, state) {
this.x = x;
this.y = y;
this.state = state;
this.nextState = this.state;
this.neighbors = [];
this.addNeighbors = this.addNeighbors.bind(this);
this.newCycle = this.newCycle.bind(this);
this.mutation = this.mutation.bind(this);
}
}
this.x
es la posición que ocupara en el eje x del tablero.this.y
es la posición que ocupara en el eje y del tablero.this.state
es el estado de nuestra casilla(Box) que puede estar viva(1) o muerta(0).this.nextState
es el siguiente estado que nuestra casilla tendera.this.neighbors
son referencias a los vecinos que rodean la casilla.
Creamos el método addNeighbors
que agregara las referencias de las casillas vecinas a la propiedad this.neighbors
. este método recibe 3 parámetros.
board
es el tablero al que pertenece.rows
el numero de filas del tablero.columns
el numero de columnas del tablero.
addNeighbors(board, rows, columns) {
let neighborX;
let neighborY;
for (let y = -1; y < 2; y++) {
for (let x = -1; x < 2; x++) {
neighborY = (this.y + y + columns) % columns;
neighborX = (this.x + x + rows) % rows;
if (y !== 0 || x !== 0) {
this.neighbors.push(board[neighborY][neighborX]);
}
}
}
}
Creamos el método newCycle
que calculara el siguiente estado de la casilla, aplicando la reglas de Conway.
newCycle() {
let neighbors = 0;
for(let i = 0; i < this.neighbors.length; i++) {
neighbors += this.neighbors[i].state;
}
// MUERTE: tiene menos de 2 o más de 3
if(neighbors < 2 || neighbors > 3) {
this.nextState = 0;
}
// VIDA/REPRODUCCIÓN: tiene exactamente 3 vecinos
if(neighbors == 3) {
this.nextState = 1;
}
}
Por ultimo creamos el método mutation
que se encarga de aplicar el estado siguiente, al estado actual.
mutation() {
this.state = this.nextState;
}
Ahora vamos al archivo board.js
donde crearemos la función Board
que se encargara de crear el tablero, y agregar la referencia de las casillas vecinas a la casilla actual. Esta función recibe 3 parámetros:
rows
numero de filas.columns
numero de columnasinitialState
un estado inicial que debe ser una matriz con valores de 0 o 1.
function Board(rows, columns, initialState) {
let board = new Array(rows);
for (let y = 0; y < columns; y++) {
board[y] = new Array(columns);
for (let x = 0; x < columns; x++) {
try {
if(initialState[y][x] === 1) {
board[y][x] = new Box(x, y, 1);
} else {
board[y][x] = new Box(x, y, 0);
}
}
catch (error) {
board[y][x] = new Box(x, y, 0);
}
}
}
for (let y = 0; y < columns; y++) {
for (let x = 0; x < rows; x++) {
board[y][x].addNeighbors(board, rows, columns);
}
}
return board;
};
Ahora pasaremos al ultimo archivo main.js
y creamos la función startGame
dentro inicializamos las variables que el juego necesita. Esta función recibe un canvas donde se dibujara el juego.
canvas
es la referencia al elemento canvas del html.
function startGame(canvas) {
const ctx = canvas.getContext('2d');
const fps = 10;
const columns = 50;
const rows = 50;
const canvasX = 500;
const canvasY = 500;
canvas.width = canvasX;
canvas.height = canvasY;
const sizeCellX = canvasX / rows;
const sizeCellY = canvasY / columns;
const initialState = [
[0,0,0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,1,1,0,0,0,0,0,0],
[0,0,0,0,0,1,1,0,0,0,0,0],
[0,0,0,0,0,1,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0,0,0],
];
const board = new Board(rows, columns, initialState);
setInterval(() => {
// Limpiar canvas
canvas.width = canvas.width;
canvas.height = canvas.height;
// Pinta estado actual
for (let y = 0; y < rows; y++) {
for (let x = 0; x < columns; x++) {
if(board[y][x].state === 1) {
ctx.fillStyle = '#fff';
} else {
ctx.fillStyle = '#000 ';
}
ctx.fillRect(y * sizeCellY, x * sizeCellX, sizeCellY, sizeCellX,);
}
}
// Calcula siguiente estado
for (let y = 0; y < rows; y++) {
for (let x = 0; x < columns; x++) {
board[y][x].newCycle();
}
}
// Aplica siguiente estado
for (let y = 0; y < rows; y++) {
for (let x = 0; x < columns; x++) {
board[y][x].mutation();
}
}
}, fps / 1000);
}
ctx
es el contexto en 2d donde pintaremos el juegofps
son las veces que el tablero se pintara en pantallarows
numero de filascolumns
numero de columnascanvasX
tamaño del canvas a lo anchocanvasY
tamaño del canvas a lo altosizeCellX
tamaño de cualquier casilla a lo anchosizeCellY
tamaño de cualquier casilla a lo altoinitialState
un estado inicial para nuestro tableroboard
creamos el tablero
Después de definir la función startGame
solo queda obtener la referencia del canvas y ejecutar la función startGame
y pasarle el canvas como parámetro.
const canvas = document.getElementById('life-game');
startGame(canvas);
y listo nos aseguramos de haber guardado todos los archivos bien y abrimos el archivo index.html
en un navegador.
También puedes ver el repositorio donde se encuentra el código fuente click aqui