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.xes la posición que ocupara en el eje x del tablero.this.yes la posición que ocupara en el eje y del tablero.this.statees el estado de nuestra casilla(Box) que puede estar viva(1) o muerta(0).this.nextStatees el siguiente estado que nuestra casilla tendera.this.neighborsson 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.
boardes el tablero al que pertenece.rowsel numero de filas del tablero.columnsel 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:
rowsnumero de filas.columnsnumero de columnasinitialStateun 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.
canvases 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);
}
ctxes el contexto en 2d donde pintaremos el juegofpsson las veces que el tablero se pintara en pantallarowsnumero de filascolumnsnumero de columnascanvasXtamaño del canvas a lo anchocanvasYtamaño del canvas a lo altosizeCellXtamaño de cualquier casilla a lo anchosizeCellYtamaño de cualquier casilla a lo altoinitialStateun estado inicial para nuestro tableroboardcreamos 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