1. Juego de la vida

El Juego de la vida es un autómata celular diseñado por el matemático británico John Horton Conway en 1970. Es un juego de cero jugadores, en el que su evolución es determinada por un estado inicial, sin requerir intervención adicional. Se considera un sistema Turing completo que puede simular cualquier otra Máquina de Turing.

Desde su publicación, ha atraído mucho interés debido a la gran variabilidad de la evolución de los patrones. Se considera que el Juego de la vida es un buen ejemplo de emergencia y autoorganización. Es interesante para científicos, matemáticos, economistas y otros observar cómo patrones complejos pueden provenir de la implementación de reglas muy sencillas.

El Juego de la vida tiene una variedad de patrones reconocidos que provienen de determinadas posiciones iniciales. Poco después de la publicación, se descubrieron el pentaminó R, el planeador o caminador (en inglés, glider, conjunto de células que se desplazan) y el explosionador (células que parecen formar la onda expansiva de una explosión).

Para muchos aficionados, el juego de la vida solo era un desafío de programación y una manera divertida de usar ciclos de la CPU. Para otros, sin embargo, el juego adquirió más connotaciones filosóficas.

2. Reglas del juego

Se trata de un juego de cero jugadores, lo que quiere decir que su evolución está determinada por el estado inicial y no necesita ninguna entrada de datos posterior. El “tablero de juego” es una malla plana formada por cuadrados (las “células”) que se extiende por el infinito en todas las direcciones. Por tanto, cada célula tiene 8 células “vecinas”, que son las que están próximas a ella, incluidas las diagonales. Las células tienen dos estados: están “vivas” o “muertas” (o “encendidas” y “apagadas”). El estado de las células evoluciona a lo largo de unidades de tiempo discretas (se podría decir que por turnos). El estado de todas las células se tiene en cuenta para calcular el estado de las mismas al turno siguiente. Todas las células se actualizan simultáneamente en cada turno, siguiendo estas reglas:

  • Una célula muerta con exactamente 3 células vecinas vivas “nace” (es decir, al turno siguiente estará viva).
  • Una célula viva con 2 o 3 células vecinas vivas sigue viva, en otro caso muere (por “soledad” o “superpoblación”)

3. Lógica del juego

  • Inicializar las celdas en la cuadrícula.
  • En cada paso de tiempo en la simulación, para cada celda (x, y) en la cuadrícula, haga lo siguiente:
    • Actualice el valor de la celda (x, y) en función de sus vecinos, teniendo en cuenta los límites de la cuadrícula.
    • Actualice la visualización de los valores de la cuadrícula.

4. Creación del programa paso a paso

4.1. Clase para el juego y constructor

Todo el juego se encuentra contenido en una única clase GameOfLife, el constructor sobrecargado de la clase recibirá como argumentos opcionales los parámetros del juego necesarios para ajustar su funcionamiento y los guardará en atributos internos:

  • width: Ancho de la cuadrícula.
  • height: Altura.
  • init_alive_cells_num: Número de células vivas en el tablero inicial.
  • game_turns: Número de turnos.

Con el alto y ancho el constructor creará una rejilla grid (atributo de la clase) vacía formada por una lista con listas anidadas (inicialmente usaremos el ‘.’ para indicar una celda vacía y poder pintarla).

Para que nuestro programa sea más flexible y poder cambiar los parámetros opcionales del constructor y cualquier otra variable que usemos para condicionar el funcioamiento del juego definiremos varias constantes en la cabecera del archivo antes de la declaración de la clase:

  • DEFAULT_CELL_WIDTH: Ancho por defecto de la rejilla (por ejemplo 10 para realizar pruebas).
  • DEFAULT_CELL_HEIGHT: Alto por defecto de la rejilla (por ejemplo 10).
  • DEFAULT_INIT_ALIVE_CELLS_NUM: Número de celulas vivas al inicio de la partida por defecto (Ej: 10).
  • DEFAULT_GAME_TURNS: Número de turnos por defecto (Ej:10).
  • ALIVE: Carácter usado para las células vivas (”*“).
  • DEAD: Carácter usado para las células muertas (“.” o caracter vacio “ ” a vuestra elección).

4.2. Pintar rejilla (draw_grid)

El método draw_grid se encargará de recorrer la rejilla, height (filas) y width (columnas) que hemos guardado como atributos internos en el constructor y pintarla (por el momento con células muertas sólo).

Ejemplo rejilla 10x10:

4.3. Celdas vivas al inicio del juego (set_init_alive_cells)

Ahora usando el atributo (init_alive_cells_num) que hemos recibido en el constructor como argumento debemos obtener posiciones aleatorias (X,y) para el número de celculas vivas usando el método set_init_alive_cells.

Recorremos en un bucle FOR en un rango con el número de celulas vivas, para cada una generamos un posición (x,y) aleatoria dentro de la rejilla con randint (from random import randint), si la celda ya está ocupada con una celula viva deberá obtener una nueva posición hasta encontrar una “vacia”.

Ejemplo rejilla 10x10: Hemos creado la instancia de la clase GameOfLife, con el método set_init_alive_cells hemos obtenido las celulas vivas en posiciones aleatorias, finalmente hemos pintado la rejilla inicial del juego con el método draw_grid:

4.4. Obtener rejilla para el siguiente turno (next_turn_grid)

El método next_turn_grid recorrerá todas las posiciones de la rejilla y para cada posición (x,y) llamará al método interno get_cell_living_neighbors para obtener cuantos “vecinos” vivos tiene esa posición.

  • Si la celula está ALIVE:
    • Tiene menos de 2 vecinos vivos pasará a estar muerta en la nueva rejilla (next_grid).
    • Tiene 2 o 3 vecinos vivos, entonces sigue viva.
    • Si tiene más de 3 entonces se ahoga y muere.
  • En caso contrario (está DEAD):
    • Tiene 3 vecinos vivos ¡vuelve a la vida!.

Cuando acabe de recorrer toda la rejilla sustituira los contenidos de la rejilla actual en grid con los recién calculados en next_grid.

4.5. Obtener los celulas vecinas vivas (get_cell_living_neighbors)

El método get_cell_living_neighbors (invocado desde el método next_turn_grid) recibe como parámetros las coordenadas (x,y) de una celda y retorna el número de celulas vivas. El método debe tomar en cuenta las posiciones frontera o límite de la cuadrícula, por ejemplo si estamos analizando la posición (0,0), no calcularemos los vecinos vivos de la fila superior (esquina superior izquierda, arriba y esquina superior derecha) ni de la columna izquierda.

Llegados a este punto podemos simular un par de turnos para ver que cambia de forma dinámica la población de celulas vivas.

  • Creamos una instancia de la clase GameOfLife.
  • Establecemos las celulas vivas con el método de la clase set_init_alive_cells.
  • Dibujamos la rejilla con draw_grid.
  • Calculamos la rejilla del siguiente turno con next_turn_grid.
  • Dibujamos la rejilla con draw_grid.

4.6. Métodos adicionales

4.6.1. Limpiar pantalla

Para poder admirar correctamente nuestra población dinámica de celulas vamos a borrar el contenido de la pantalla antes de volver a pintar la rejilla, así el cambio de rejilla en cada turno será encima de la anterior.

from os import system, name

#...

class GameOfLife:  

	# ....

	def __clear(self):
        # for windows
        if name == 'nt':
            _ = system('cls')
        # for mac and linux(here, os.name is 'posix')
        else:
            _ = system('clear')

4.6.2. Esperar un tiempo

Entre turno y turno realizaremos una espera de 1 segundo para que no vaya demasiado rápido:

import time

#...

class GameOfLife:  

	# ....

	def __wait(self):
        time.sleep(1)

4.7. Lógica del juego (init_game)

El método init_game de la clase GameOfLife será el responsable de desencadenar la evolución. Primero calculará las posiciones aleatorias de las celulas vivas en la rejilla set_init_alive_cells, luego mientras el contador de turnos no alcance el límite establecido en el constructor (game_turns) haremos lo siguiente:

  • clear: Limpiar la pantalla
  • Imprimir el nº de turno.
  • draw_grid: Imprimir la rejilla.
  • next_turn_grid: Calcular la rejilla del siguiente turno.
  • wait: Esperar

El juego en acción, una rejilla 10x10, con 10 células vivas al inicio y jugando 10 turnos, he usado el espacio ‘ ’ (constante DEAD) para pintar las células muertas y poder apreciar mejor los patrones de las vivas.