Y llegamos por fin a la última entrada del curso Fundamentos de la Programación. En esta ocasión vamos a programar el juego LIFE, uno de los primeros juegos en ser ampliamente distribuido entre la comunidad. Creo que resulta en un ejercicio interesante, y aunque aquí lo encontrarás todo guiado, ¡intenta hacerlo por ti mismo!

Una Simulación de la Vida Celular

LIFE fue diseñado por John Conway y publicado en la revista “Scientific American”. El juego simula la vida y muerte de un conjunto de células, a partir de unas reglas simples. No obstante, la combinación de las mismas da origen a hermosos y variantes patrones. Las reglas que siguen estas células son:

  1. Nacimiento: una célula muerta resucitará si tiene exactamente tres vecinos vivos.
  2. Supervivencia: una célula permanecerá con vida si tiene dos o tres vecinos.
  3. Superpoblación: una célula morirá o permanecerá muerta si tiene cuatro o más vecinos.
  4. Aislamiento: una célula morirá o permanecerá muerta si tiene menos de dos vecinos.

En nuestra implementación utilizaremos una matriz para gestionar el tablero, y el mismo irá evolucionando durante una serie de pulsos de reloj. La dimensión del tablero estará definida por dos constantes, de forma que podremos modificarla sin problemas más adelante de ser necesario. El estado de las células lo definiremos con los valores lógicos True (viva) y False (muerta). La siguiente función genera un tablero de 25×25 celdas:

# Variables Globales
FIL = 10
COL = 10
tablero = []
# Inicializa un tablero sin ninguna célula
def inicializaTablero():
	for i in range(FIL):
		tablero.append([False] * COL)

Ahora que ya tenemos inicializado nuestro tablero sin células vivas, debemos introducir en el mismo algunas, ya que si no, nuestro pequeño universo no comenzará a “evolucionar”. Aquí podríamos elegir diversos patrones inicialesy cada uno generaría una secuencia distinta. Por simplicidad, vamos a iniciar el juego con una línea vertical de tres células:

# Patrones para inicializar el tablero
def generaLinea():
	tablero[12][13] = True
	tablero[13][13] = True
	tablero[14][13] = True

Lo siguiente es poder visualizar el estado del tablero por pantalla. En este ejemplo vamos a utilizar la salida estándar por consola, dado que en el curso no se ha visto nada de programación gráfica (si estáis interesados, indicarlo y le pondremos remedio :P). Hemos definido una casilla con una célula viva con el carácter “X”, y una vacía o muerta con el carácter “·”:

# Muestra el tablero por pantalla
def muestraTablero():
	for i in range(FIL):
		for j in range(COL):
			if tablero[i][j]:
				print("X", end = '')
			else:
				print("·", end = '')
		print()

¿Te llama algo la atención? En algunas funciones print() hemos añadido la variable end. Esta sirve para indicar que carácter queremos utilizar como final de línea. Por defecto viene un salto de línea, pero como para este ejemplo nos interesa poder visualizar toda una línea de la matriz de forma continua, se ha modificado por un carácter nulo.

Bien, ya tenemos todo lo necesario para comenzar a generar vida en nuestro mundo. Para poder decidir si se deben aplicar o no las reglas antes descritas, necesitamos conocer el número de vecinos de una célula en concreto. Dado que es un cálculo que se ha de repetir mucho, es recomendable crearnos una función para ello:

# Calcula el número de vecinos de una célula
def calculaVecinos(fila, columna):
    vecinos = 0
    # Vecinos en la columna anterior
    if fila > 0 and columna > 0 and tablero[fila-1][columna-1]:
        vecinos += 1
    if columna > 0 and tablero[fila][columna-1]:
        vecinos += 1
    if fila < FIL-1 and columna > 0 and tablero[fila+1][columna-1]:
        vecinos += 1
    # Vecinos en la misma columna
    if fila > 0 and tablero[fila-1][columna]:
        vecinos += 1
    if fila < FIL-1 and tablero[fila+1][columna]:
        vecinos += 1
    # Vecinos en la columna siguiente
    if fila > 0 and columna < COL-1 and tablero[fila-1][columna+1]:
        vecinos += 1
    if columna < COL-1 and tablero[fila][columna+1]:
        vecinos += 1
    if fila < FIL-1 and columna < COL-1 and tablero[fila+1][columna+1]:
        vecinos += 1
    return vecinos

Cuidado a la hora de comprobar los vecinos, ya que si la célula se encuentra en el borde del mundo, habrán posiciones a su alrededor en las que no podrá haber ningún vecino, y si intentáramos comprobarlas, nos saldríamos del rango de la matriz y eso generaría un error. En el código anterior podéis observar como en cada sentencia if se ha comprobado también si la fila y la columna están dentro del rango.

Por último, toca aplicar las acciones correspondientes a cada regla. Para ello recorreremos el tablero casilla a casilla, comprobando si alguna de las condiciones se cumple. La función resultante sería algo así:

# Calcula los nuevos estados de las células tras un pulso de reloj
def avanzaPulso():
    for i in range(FIL):
        for j in range(COL):
            n = calculaVecinos(i, j)
            # Nacimiento
            if not tablero[i][j] and n == 3:
                tablero[i][j] = True
            # Supervivencia
            elif tablero[i][j] and (n == 3 or n == 2):
                tablero[i][j] = True
            # Superpoblación y Aislamiento
            else:
                tablero[i][j] = False

¡Ya tenemos listo nuestro código! Sólo nos falta definir el programa principal, el cual consistirá en un bucle que se ejecutara un número determinado de pulsos. Para ello hemos declarado una nueva variable que almacena el número total de pulsos que se debe ejecutar:

# Programa Principal
inicializaTablero()
generaLinea()
for t in range(pulsos):
    print("PULSO: ", t+1)
    muestraTablero()
    avanzaPulso()

Bien. ¿Has llegado a ejecutar este código? ¿Observas algo raro? ¡El tablero está vacío! ¿Sabes por qué ha ocurrido? La causa está en que hemos estado comprobando el estado del tablero a la vez que lo estamos modificando por aplicar las condiciones definidas. Esto causa que durante un mismo pulso se estén matando y resucitando células. Una solución sencilla es realizar la actualización de las células sobre un nuevo tablero, y al finalizar sobreescribir el contenido del tablero original. Con esta modificación, la función avanzaPulso() quedaría así:

# Calcula los nuevos estados de las células tras un pulso de reloj
def avanzaPulso():
    global tablero
    copia = []
    for i in range(FIL):
        copia.append([False] * COL)
        
    for i in range(FIL):
        for j in range(COL):
            n = calculaVecinos(i, j)
            # Nacimiento
            if not tablero[i][j] and n == 3:
                copia[i][j] = True
            # Supervivencia
            elif tablero[i][j] and (n == 3 or n == 2):
                copia[i][j] = True
            # Superpoblación y Aislamiento
            else:
                copia[i][j] = False
                
    tablero = copia

Ahora nuestro programa ya funciona correctamente. A continuación te dejo el código completo del mismo. Puedes probar a modificar el patrón inicial y el número de pulsos, a ver que resultados obtienes.

# Variables Globales
FIL = 25
COL = 25
pulsos = 5
tablero = []
# Inicializa un tablero sin ninguna celula
def inicializaTablero():
	for i in range(FIL):
		tablero.append([False] * COL)
# Patrones para inicializar el tablero
def generaLinea():
	tablero[12][13] = True
	tablero[13][13] = True
	tablero[14][13] = True
# Muestra el tablero por pantalla
def muestraTablero():
	for i in range(FIL):
		for j in range(COL):
			if tablero[i][j]:
				print("X", end = '')
			else:
				print("·", end = '')
		print()
# Calcula el número de vecinos de una célula
def calculaVecinos(fila, columna):
    vecinos = 0
    # Vecinos en la columna anterior
    if fila > 0 and columna > 0 and tablero[fila-1][columna-1]:
        vecinos += 1
    if columna > 0 and tablero[fila][columna-1]:
        vecinos += 1
    if fila < FIL-1 and columna > 0 and tablero[fila+1][columna-1]:
        vecinos += 1
    # Vecinos en la misma columna
    if fila > 0 and tablero[fila-1][columna]:
        vecinos += 1
    if fila < FIL-1 and tablero[fila+1][columna]:
        vecinos += 1
    # Vecinos en la columna siguiente
    if fila > 0 and columna < COL-1 and tablero[fila-1][columna+1]:
        vecinos += 1
    if columna < COL-1 and tablero[fila][columna+1]:
        vecinos += 1
    if fila < FIL-1 and columna < COL-1 and tablero[fila+1][columna+1]:
        vecinos += 1
    return vecinos
        
# Calcula los nuevos estados de las células tras un pulso de reloj
def avanzaPulso():
    global tablero
    copia = []
    for i in range(FIL):
        copia.append([False] * COL)
        
    for i in range(FIL):
        for j in range(COL):
            n = calculaVecinos(i, j)
            # Nacimiento
            if not tablero[i][j] and n == 3:
                copia[i][j] = True
            # Supervivencia
            elif tablero[i][j] and (n == 3 or n == 2):
                copia[i][j] = True
            # Superpoblación y Aislamiento
            else:
                copia[i][j] = False
                
    tablero = copia
# Programa Principal
inicializaTablero()
generaLinea()
for t in range(pulsos):
    print("PULSO: ", t+1)
    muestraTablero()
    avanzaPulso()

¿Os ha gustado el ejercicio? El patrón que hemos utilizado es uno de los denominados osciladores, que son patrones que tras un número de iteraciones regresan a su estado inicial. Aquí os dejo algunos patrones para ir probando.

¡Nos leemos!


¿Quieres estar al día de todo lo publicado en el blog?

Para ello puedes suscribirte a mi canal RSS y/o seguirme en mis Redes Sociales.

Etiquetas: , , ,

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Esta web utiliza cookies propias y de terceros para su correcto funcionamiento y para fines analíticos. Contiene enlaces a sitios web de terceros con políticas de privacidad ajenas que podrás aceptar o no cuando accedas a ellos. Al hacer clic en el botón Aceptar, acepta el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Más información
Privacidad