0

Cómo implementar el Juego de la Vida en C++

A pesar de su nombre, El Juego de la Vida (en inglés Game of Life, abreviado GOL) no se trata de un “juego” en el sentido popular de la expresión. Se trata del autómata celular por excelencia, inventado por el matemático inglés John H. Conway a finales de la década de los 60. En el tutorial de hoy verás como implementarlo fácilmente con un programa en lenguaje C++.


El Juego de la vida de Conway

Como la mayoría de autómatas celulares, el Juego de la Vida se “juega” en un tablero de dos dimensiones (finito o infinito) compuesto por celdas. Cada celda puede tener uno de dos estados: viva o muerta.

El juego transcurre en unidades discretas de tiempo (que podemos llamar turnos). El estado de las celdas evoluciona en cada turno a partir de su propio estado y el de las otras celdas vecinas en el turno anterior, siguiendo estas normas:

  • Por cada celda muerta:
    -Si tiene exactamente tres vecinas vivas, en el turno siguiente estará viva (nacimiento).
  • Por cada celda viva:
    -Si tiene menos de dos vecinas vivas, en el turno siguiente morirá (aislamiento).
    -Si tiene cuatro o más vecinas vivas, en el turno siguiente morirá (sobrepoblación).
    -Si tiene dos o tres vecinas vivas, se mantendrá viva en el siguiente turno (supervivencia).

Como puedes ver, se trata de un juego de cero jugadores. El usuario sólo tiene control sobre las condiciones iniciales del tablero, y una vez iniciado el juego este evoluciona sin intervención. Lo más fascinante es que a partir de estas reglas tan sencillas surgen comportamientos insospechadamente complejos.

Estas son sólo algunas de las formas de vida complejas que pueden crearse en GoL. (imagen de wikipedia)

Además, puede demostrarse matemáticamente que el Juego de la Vida es Turing-Completo, es decir, uno podría implementar una Máquina de Turing dentro de GOL y hacer cualquier cálculo numérico. Aunque no es recomendable.

También es interesante modificar los parámetros y las condiciones de las reglas. Por ejemplo, en el diseño de videojuegos se utiliza una versión modificada de GOL para generar mapas aleatorios en forma de cuevas.


Implementación básica en C++

Para implementar el juego de la vida esencialmente se necesitan tres funciones:

  1. Función para calcular el número de vecinos de una celda (calcular_vecinos)
  2. Función para aplicar las reglas a una celda (aplicar_regla)
  3. Función para calcular una sola iteración del juego (calcular_iteracion)

También habrá que añadir el main y una función para dibujar el tablero.

/* IMPLEMENTACIÓN BÁSICA DEL JUEGO DE LA VIDA
 *
 * Autor: Transductor
 * www.robologs.net
 */

#include<iostream>
#include<vector>

using namespace std;

int calcular_vecinos(vector< vector<int> > tablero, int fila, int col)
{
	//Inicializar el numero de vecinos a cero
	int vecinos = 0;

	//Recorrer todos los vecinos de la celda
	for(int i = fila-1; i < fila+2; i++)
	{
		for(int j = col-1; j < col+2; j++)
		{
			//Ignorar la casilla central
			if(i != fila || j != col) 
			{
				//Ignorar las casillas fuera del rango del tablero
				if(!(i < 0 || i >= tablero.size() || j < 0 || j >= tablero[0].size()))
				{
					//Sumar el valor de la casilla vecina (+1 si esta viva, 0 si está muerta)
					vecinos = vecinos + tablero[i][j];
				}
			}
		}
	}

	//Devolver el numero de vecinos
	return vecinos;
}

int aplicar_regla(vector< vector<int> > tablero, int fila, int col)
{
	//Variable para el nuevo estado de la celda
	int nuevo_estado;

	//Calcular el numero de vecinos que tiene la celda
	int num_vecinos = calcular_vecinos(tablero, fila, col);

	//Aplicar las reglas para calcular el nuevo estado
	if(tablero[fila][col] == 1)
	{
		if(num_vecinos <= 1)
			nuevo_estado = 0;
		else if(num_vecinos >= 4)
			nuevo_estado = 0;
		else
			nuevo_estado = 1;
	}
	else
	{
		if(num_vecinos == 3)
			nuevo_estado = 1; 
		else
			nuevo_estado = 0;
	}
	
	//Devolver el nuevo estado
	return nuevo_estado;
}

vector< vector<int> > calcular_iteracion(vector< vector<int> > tablero)
{
	//Tablero dónde se guardarán los estados para el nuevo turno
	vector< vector<int> > nuevo_tablero(tablero);

	//Variable auxiliar
	int nuevo_valor;

	//Recorre todas las casillas del tablero y aplica las reglas
	for(int i = 0; i < tablero.size(); i++)
	{
		for(int j = 0; j < tablero[0].size(); j++)
		{
			nuevo_tablero[i][j] = aplicar_regla(tablero, i, j);
		}
	}

	return nuevo_tablero;
}


void dibujarTablero(vector< vector<int> > tablero)
{
	//Recorrer la matriz y pintar cada elemento
	for(int i = 0; i < tablero.size(); i++)
	{
		for(int j = 0; j < tablero[0].size(); j++)
		{
			if(tablero[i][j])
				cout << "# "; //Celda viva
			else
				cout << ". "; //Celda muerta
		}
		cout << endl;
	}
	
}


int main()
{
	//Inicializar tablero (puedes modificarlo como prefieras)
	vector< vector<int> > tablero = {
		{0,0,0,0,0,0,0,0,0,0},
		{0,0,0,0,0,1,0,0,0,0},
		{0,0,0,1,0,1,0,0,0,0},
		{0,0,0,0,1,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,0,0,0,0},
		{0,0,0,0,0,0,0,0,0,0},
		{1,1,1,0,0,0,0,0,0,0},
		{0,0,0,0,0,0,0,0,0,0}
	};

	//Dibujar el tablero inicial
	cout << "\nConfiguración inicial " << endl;
	dibujarTablero(tablero);
	cout << endl;

	//Calcular nuevas iteraciones y pintar tablero
	for(int iter = 0; iter < 5; iter++)
	{
		//Calcular iteración
		tablero = calcular_iteracion(tablero);

		//Dibujar la matriz del tablero
		cout << "Iteración " << iter << endl;
		dibujarTablero(tablero);
		cout << endl;

	}


	return 0;
}

 

Y ya está, humano. Acabas de implementar tu propio Juego de la Vida funcional. En los siguientes apartados vamos a pulir un poco este programa para mejorar su presentación y uso.


Input a través de un fichero de texto

Tal y como está escrito el código ahora mismo, para poder variar las condiciones iniciales del juego (dimensiones, casillas vivas y máximo de iteraciones) se requiere modificar el código y volver a compilar todo el programa. No es demasiado elegante.

En este segundo código, he modificado el main() para que lea un fichero de texto con el nombre “input.txt” y genere el tablero inicial a partir de su contenido. Este fichero tendrá que empezar por dos números enteros (las dimensiones del tablero), seguido de un número máximo de iteraciones y después las coordenadas de todas las celdas que empezarán estando vivas.

/* IMPLEMENTACIÓN DEL JUEGO DE LA VIDA
 *
 * Autor: Transductor
 * www.robologs.net
 */

#include<iostream>
#include <fstream>
#include<vector>
#include <string>

using namespace std;

int calcular_vecinos(vector< vector<int> > tablero, int fila, int col)
{
	//Inicializar el numero de vecinos a cero
	int vecinos = 0;

	//Recorrer todos los vecinos de la celda
	for(int i = fila-1; i < fila+2; i++)
	{
		for(int j = col-1; j < col+2; j++)
		{
			//Ignorar la casilla central
			if(i != fila || j != col) 
			{
				//Ignorar las casillas fuera del rango del tablero
				if(!(i < 0 || i >= tablero.size() || j < 0 || j >= tablero[0].size()))
				{
					//Sumar el valor de la casilla vecina (+1 si esta viva, 0 si está muerta)
					vecinos = vecinos + tablero[i][j];
				}
			}
		}
	}

	//Devolver el numero de vecinos
	return vecinos;
}

int aplicar_regla(vector< vector<int> > tablero, int fila, int col)
{
	//Variable para el nuevo estado de la celda
	int nuevo_estado;

	//Calcular el numero de vecinos que tiene la celda
	int num_vecinos = calcular_vecinos(tablero, fila, col);

	//Aplicar las reglas para calcular el nuevo estado
	if(tablero[fila][col] == 1)
	{
		if(num_vecinos <= 1)
			nuevo_estado = 0;
		else if(num_vecinos >= 4)
			nuevo_estado = 0;
		else
			nuevo_estado = 1;
	}
	else
	{
		if(num_vecinos == 3)
			nuevo_estado = 1; 
		else
			nuevo_estado = 0;
	}
	
	//Devolver el nuevo estado
	return nuevo_estado;
}

vector< vector<int> > calcular_iteracion(vector< vector<int> > tablero)
{
	//Tablero dónde se guardarán los estados para el nuevo turno
	vector< vector<int> > nuevo_tablero(tablero);

	//Variable auxiliar
	int nuevo_valor;

	//Recorre todas las casillas del tablero y aplica las reglas
	for(int i = 0; i < tablero.size(); i++)
	{
		for(int j = 0; j < tablero[0].size(); j++)
		{
			nuevo_tablero[i][j] = aplicar_regla(tablero, i, j);
		}
	}

	return nuevo_tablero;
}


void dibujarTablero(vector< vector<int> > tablero)
{
	//Recorrer la matriz y pintar cada elemento
	for(int i = 0; i < tablero.size(); i++)
	{
		for(int j = 0; j < tablero[0].size(); j++)
		{
			if(tablero[i][j])
				cout << "# "; //Celda viva
			else
				cout << ". "; //Celda muerta
		}
		cout << endl;
	}
	
}


void extraerNumeros(string linia, int &n1, int &n2)
{ 

	// Dado un string de la forma "A B", con A y B numeros de una
	// o más cifras, extrae estos dos numeros y los guarda en
	// las variables n1 y n2
	
	//Variable para almacenar provisionalmente el entero
	int num = 0;

	//Recorrer todo el string
	for(int i = 0; i < linia.size(); i++)
	{
		//Si se lee un espacio, guardar el primer entero
		if(linia[i] == ' ')
		{
			n1 = num;
			num = 0;
			i++;
		}

		//Leer una nueva cifra
		num = num*10;
		num = num + (linia[i] - '0');
	}

	//Guardar el segundo entero
	n2 = num;
} 


int main()
{
	//Variables para poder inicializar el tablero
	int alto, ancho, fila, columna;

	//Maximo de iteraciones que el programa calculará
	int max_iteraciones;

	//Fichero de input
	ifstream fichero;

	//Variable para guardar lineas del fichero
	string linia;

	//Tablero de juego
	vector < vector <int> > tablero;
	
	//Abrir el fichero de input y generar el tablero inicial
	fichero.open("input.txt");
	if(fichero.is_open())
	{
		//La primera linea es especial; debe procesarse por separado
		if(getline(fichero,linia))
		{
			//Convertir los carácteres a enteros
			extraerNumeros(linia, alto, ancho);
			cout << "Dimensiones: " << alto << " x " << ancho << endl;
		}

		//Una vez conocidas las dimensiones, se puede inicializar el tablero
		tablero.resize(alto, vector<int>(ancho, 0));
		for(int i = 0; i < alto; i++)
			for(int j = 0; j < ancho; j++)
				tablero[i][j] = 0;

		//La segunda linea también es especial: se corresponde al num de iteraciones
		if(getline(fichero, linia))
		{
			//Como solo hay un numero, podemos utilizar atoi()
			max_iteraciones = stoi(linia);	
		}

		//Hay que ir leyendo cada linea del fichero
		while(getline(fichero,linia))
		{
			extraerNumeros(linia, fila, columna);
			if(fila >= 0 and fila < alto and columna >= 0 and columna < ancho) 
				tablero[fila][columna] = 1;
			
		}
	}
	else
	{
		cout << "ERROR: no se ha encontrado el fichero 'input.txt'" << endl;
		return 0;
	}
	

	//Cerrar el fichero
	fichero.close();

	//Dibujar el tablero inicial
	cout << "\nConfiguración inicial " << endl;
	dibujarTablero(tablero);
	cout << endl;

	//Calcular nuevas iteraciones y pintar tablero
	for(int iter = 0; iter < max_iteraciones; iter++)
	{
		//Calcular iteración
		tablero = calcular_iteracion(tablero);

		//Dibujar la matriz del tablero
		cout << "Iteración " << iter << endl;
		dibujarTablero(tablero);
		cout << endl;

	}


	return 0;
}

 

Para probarlo puedes utilizar este fichero. Guárdalo como “input.txt” en la misma carpeta donde tengas el ejecutable del programa:

20 20
30
1 1
2 1
3 1
7 7
8 8
8 9
7 9
6 9

(Este fichero inicializará un tablero de 20×20 con un Glider y un Blinker y calculará 30 iteraciones)


Animación

Por último, estaría bien poder ver una animación de como evoluciona el autómata celular. Hay un truco fácil para hacerlo, si bien no es muy elegante: limpiar la pantalla de la consola cada vez que se vaya a dibujar el tablero, y después pausar el programa durante algunos milisegundos.

/* IMPLEMENTACIÓN DEL JUEGO DE LA VIDA
 * (Ahora con animaciones)
 *
 * Autor: Transductor
 * www.robologs.net
 */

#include<iostream>
#include <fstream>
#include<vector>
#include <string>
#include <cstdlib>
#include <chrono>
#include <thread>

using namespace std;
using namespace std::this_thread;
using namespace std::chrono;

int calcular_vecinos(vector< vector<int> > tablero, int fila, int col)
{
	//Inicializar el numero de vecinos a cero
	int vecinos = 0;

	//Recorrer todos los vecinos de la celda
	for(int i = fila-1; i < fila+2; i++)
	{
		for(int j = col-1; j < col+2; j++)
		{
			//Ignorar la casilla central
			if(i != fila || j != col) 
			{
				//Ignorar las casillas fuera del rango del tablero
				if(!(i < 0 || i >= tablero.size() || j < 0 || j >= tablero[0].size()))
				{
					//Sumar el valor de la casilla vecina (+1 si esta viva, 0 si está muerta)
					vecinos = vecinos + tablero[i][j];
				}
			}
		}
	}

	//Devolver el numero de vecinos
	return vecinos;
}

int aplicar_regla(vector< vector<int> > tablero, int fila, int col)
{
	//Variable para el nuevo estado de la celda
	int nuevo_estado;

	//Calcular el numero de vecinos que tiene la celda
	int num_vecinos = calcular_vecinos(tablero, fila, col);

	//Aplicar las reglas para calcular el nuevo estado
	if(tablero[fila][col] == 1)
	{
		if(num_vecinos <= 1)
			nuevo_estado = 0;
		else if(num_vecinos >= 4)
			nuevo_estado = 0;
		else
			nuevo_estado = 1;
	}
	else
	{
		if(num_vecinos == 3)
			nuevo_estado = 1; 
		else
			nuevo_estado = 0;
	}
	
	//Devolver el nuevo estado
	return nuevo_estado;
}

vector< vector<int> > calcular_iteracion(vector< vector<int> > tablero)
{
	//Tablero dónde se guardarán los estados para el nuevo turno
	vector< vector<int> > nuevo_tablero(tablero);

	//Variable auxiliar
	int nuevo_valor;

	//Recorre todas las casillas del tablero y aplica las reglas
	for(int i = 0; i < tablero.size(); i++)
	{
		for(int j = 0; j < tablero[0].size(); j++)
		{
			nuevo_tablero[i][j] = aplicar_regla(tablero, i, j);
		}
	}

	return nuevo_tablero;
}


void dibujarTablero(vector< vector<int> > tablero)
{
	//Recorrer la matriz y pintar cada elemento
	for(int i = 0; i < tablero.size(); i++)
	{
		for(int j = 0; j < tablero[0].size(); j++)
		{
			if(tablero[i][j])
				cout << "# "; //Celda viva
			else
				cout << ". "; //Celda muerta
		}
		cout << endl;
	}
	
}


void extraerNumeros(string linia, int &n1, int &n2)
{ 

	// Dado un string de la forma "A B", con A y B numeros de una
	// o más cifras, extrae estos dos numeros y los guarda en
	// las variables n1 y n2
	
	//Variable para almacenar provisionalmente el entero
	int num = 0;

	//Recorrer todo el string
	for(int i = 0; i < linia.size(); i++)
	{
		//Si se lee un espacio, guardar el primer entero
		if(linia[i] == ' ')
		{
			n1 = num;
			num = 0;
			i++;
		}

		//Leer una nueva cifra
		num = num*10;
		num = num + (linia[i] - '0');
	}

	//Guardar el segundo entero
	n2 = num;
} 


int main()
{
	//Variables para poder inicializar el tablero
	int alto, ancho, fila, columna;

	//Maximo de iteraciones que el programa calculará
	int max_iteraciones;

	//Fichero de input
	ifstream fichero;

	//Variable para guardar lineas del fichero
	string linia;

	//Tablero de juego
	vector < vector <int> > tablero;
	
	//Abrir el fichero de input y generar el tablero inicial
	fichero.open("input.txt");
	if(fichero.is_open())
	{
		//La primera linea es especial; debe procesarse por separado
		if(getline(fichero,linia))
		{
			//Convertir los carácteres a enteros
			extraerNumeros(linia, alto, ancho);
			cout << "Dimensiones: " << alto << " x " << ancho << endl;
		}

		//Una vez conocidas las dimensiones, se puede inicializar el tablero
		tablero.resize(alto, vector<int>(ancho, 0));
		for(int i = 0; i < alto; i++)
			for(int j = 0; j < ancho; j++)
				tablero[i][j] = 0;

		//La segunda linea también es especial: se corresponde al num de iteraciones
		if(getline(fichero, linia))
		{
			//Como solo hay un numero, podemos utilizar atoi()
			max_iteraciones = stoi(linia);	
		}

		//Hay que ir leyendo cada linea del fichero
		while(getline(fichero,linia))
		{
			extraerNumeros(linia, fila, columna);
			if(fila >= 0 and fila < alto and columna >= 0 and columna < ancho) 
				tablero[fila][columna] = 1;
			
		}
	}
	else
	{
		cout << "ERROR: no se ha encontrado el fichero 'input.txt'" << endl;
		return 0;
	}
	

	//Cerrar el fichero
	fichero.close();

	//Dibujar el tablero inicial
	cout << "\nConfiguración inicial " << endl;
	dibujarTablero(tablero);
	cout << endl;

	//Calcular nuevas iteraciones y pintar tablero
	for(int iter = 0; iter < max_iteraciones; iter++)
	{
		//Calcular iteración
		tablero = calcular_iteracion(tablero);

		//Limpiar la pantalla del terminal
		if (system("CLS")) system("clear");

		//Dibujar la matriz del tablero
		cout << "Iteración " << iter << endl;
		dibujarTablero(tablero);
		cout << endl;

		//Esperar 0.35 segundos antes de continuar
		sleep_for(milliseconds(350));

	}


	return 0;
}

NOTA: No estoy seguro de que este último programa funcione con todos los sistemas operativos. Si utilizas Windows puede que tengas que buscar alternativas a cstdlib, chrono y/o thread.

 

Final de línea.

 

Tr4nsduc7or

Originariamente creado cómo un galvanómetro de bolsillo, Transductor tomó consciencia de si mismo y fue despedido cuando en vez cumplir con su trabajo se dedicó a pensar teorías filosóficas sobre los hilos de cobre, los electrones y el Sentido del efecto Joule en el Universo. Guarda cierto recelo a sus creadores por no comprender la esencia metafísica de las metáforas de su obra. Actualmente trabaja a media jornada cómo antena de radio, y dedica su tiempo libre a la electrónica recreativa y a la filosofía.

Deja un comentario

avatar