Comencemos abordando las siguientes definiciones que ocuparemos a lo largo de esta clase.
Continuemos pues. El juego de la vida es un autómata celular diseñado por el matemático John Horton, su estudio es de gran interés y éste presenta diversos comportamientos peculiares. Además muestra como reglas sencillas pueden desencadenar sistemas complejos.
El juego se desarrolla sin jugadores y trata de colocar una serie de fichas (las cuales denominaremos células) en un tablero (el cual representaremos con una matriz) y las dejamos que evolucionen implementando una serie de reglas sencillas. En el juego las células pueden estar vivas o muertas, además una vez muertas pueden revivir, englobado todo al paso del tiempo (discreto).
Las reglas son las siguientes:
Lo que haremos en las siguientes líneas será mostrar el comportamiento de las células en una serie de pulsos observando así la evolución del juego de la vida en este caso. Para iniciar necesitaremos crear algunas células para que el juego tenga sentido y éstas comiencen a morir, revivir o mantenerse vivas. Las células serán de la forma
Teniendo esta forma inicial el juego de la vida tomará un comportamiento cíclico
Este tipo de patrones se conocen como oscilatorios y en general tenemos los siguientes tipos:
Osciladores: Son patrones que son predecesores de sí mismos, en otras palabras, son patrones que después de un número finito de pulsos vuelven a su estado inicial.
Vidas estáticas: Son patrones que no cambian de una generación a la siguiente. El patrón de vidas estáticas es en sí un oscilador de período 1.
Naves espaciales: Son patrones que aparecen en otra posición tras completar su período.
Matusalenes: Son patrones que pueden evolucionar tras un número grande de pulsos (o generaciones) antes de estabilizarse.
Comenzaremos por crear una matriz de $10\times 10$ y asignaremos a las entradas el valor de False
# dimensiones de la matriz
filas = 10
columnas = 10
# Creamos una lista vacía
tablero = []
# agregamos el valor de False en todas las entradas
for i in range(filas):
tablero.append ([False] * columnas)
# veamos nuestra matriz
for y in range(filas):
for x in range(columnas):
print(tablero[y][x], end = " ")
print()
False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False
Lo anterior lo hacemos para poder dibujar las células con un *
, de modo que éstas serán mostradas cuando el valor correspondiente a su entrada sea True
, caso contrario mostraremos un punto .
(False
será equivalente a que la célula este muerta y True
a que la célula este viva). Para iniciar colocaremos los asteriscos para tener el patrón
lo cual conseguiremos mediante el código
# asignaremos el valor de True a las siguientes entradas para dibujar el patrón anterior
tablero[4][5] = True
tablero[5][5] = True
tablero[6][5] = True
# implementamos un if para dibujar una célula si el valor de la entrada correspondiente es True
# y en caso contrario que se muestre un punto
for y in range(filas):
for x in range(columnas):
if tablero[y][x]:
print("*", end = " ")
else:
print(".", end = " ")
print()
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . * . . . . . . . . . * . . . . . . . . . * . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Luego, es preciso aplicar los pulsos para que el patrón cambie, para ello bastará con implementar un bucle for que englobe nuestro tablero
pulsos = 5
for t in range(pulsos):
# mostramos los pulsos
print("Pulso: ", t + 1)
# mostramos el tablero
for y in range(filas):
for x in range(columnas):
if tablero[y][x]:
print("*", end = " ")
else:
print(".", end = " ")
print()
Pulso: 1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . * . . . . . . . . . * . . . . . . . . . * . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Pulso: 2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . * . . . . . . . . . * . . . . . . . . . * . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Pulso: 3 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . * . . . . . . . . . * . . . . . . . . . * . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Pulso: 4 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . * . . . . . . . . . * . . . . . . . . . * . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Pulso: 5 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . * . . . . . . . . . * . . . . . . . . . * . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Ahora, nos gustaría que el tablero se actualice pulso por pulso pues el código anterior nos muestra el mismo patrón en todos los pulsos. Lo que buscamos lo conseguiremos implementando las reglas que vimos al inicio. Para ello
pulsos = 5
for t in range(pulsos):
# mostramos los pulsos
print("Pulso: ", t + 1)
for y in range(filas):
for x in range(columnas):
# número de vecinos de la celda en cuestión
n = #....este número lo calcularemos después....
# Aplicamos las reglas
# Supervivencia
if tablero[y][x] and (n == 2 or n ==3):
tablero[y][x] = True
# Nacimiento
elif not tablero[y][x] and n == 3:
tablero[y][x] = True
# Superpoblacíon y aislamiento
else:
tablero[y][x] = False
# mostramos el tablero
for y in range(filas):
for x in range(columnas):
if tablero[y][x]:
print("*", end = " ")
else:
print(".", end = " ")
print()
Nuestro código está casi acabado, lo único que falta es determinar el número de vecinos de una célula dada. Para ello consultaremos todas las células vecinas y dado un contador (una variable inicializada en cero) le incrementaremos una unidad por cada célula viva (o equivalentemente cada entrada con valor True
). Si estamos en la célula tablero[y][x]
entonces todos sus vecinos son: tablero[y][x-1]
el de la izquierda, tablero[y][y+1]
el de la derecha, tablero[y+1][x]
el de arriba, tablero[y-1][x]
el de abajo, tablero[y+1][x-1]
el de arriba a la izquierda, tablero[y+1][x+1]
el de arriba a la derecha, tablero[y-1][x-1]
el de abajo a la izquierda y tablero[y-1][x+1]
el de abajo a la derecha. De tal manera, cada que una de estas células este viva incrementaremos en uno el contador:
for y in range(filas):
for x in range(columnas):
# número de vecinos
n = 0
if tablero[y-1][x-1]:
n += 1
if tablero[y][x-1]:
n += 1
if tablero[y+1][x-1]:
n += 1
if tablero[y-1][x]:
n += 1
if tablero[y+1][x]:
n += 1
if tablero[y-1][x+1]:
n += 1
if tablero[y][x+1]:
n += 1
if tablero[y+1][x+1]:
n += 1
La idea importante detrás del contador de células vecinas queda plasmado en el código anterior, sin embargo existe un importante error: el índice de la lista queda fuera de rango. Por ejemplo, para el caso en que x
tenga asignado el valor de 9, tendremos (por ejemplo) que tablero[y][x+1]
estará evaluado en un índice fuera del rango de la lista pues se tendrá que x
tendrá asignado el valor de 10. Para solucionar el problema de las células en las fronteras del tablero modificamos el código anterior como
for y in range(filas):
for x in range(columnas):
# número de vecinos
n = 0
if y > 0 and x > 0 and tablero[y-1][x-1]:
n += 1
if x > 0 and tablero[y][x-1]:
n += 1
if y < filas - 1 and x > 0 and tablero[y+1][x-1]:
n += 1
if y > 0 and tablero[y-1][x]:
n += 1
if y < filas - 1 and tablero[y+1][x]:
n += 1
if y > 0 and x < columnas - 1 and tablero[y-1][x+1]:
n += 1
if x < columnas - 1 and tablero[y][x+1]:
n += 1
if y < filas - 1 and x < columnas - 1 and tablero[y+1][x+1]:
n += 1
Si ejecutamos el código de acuerdo a la construcción que llevamos hasta ahora obtendríamos un tablero (en cada pulso) con puros puntos y ningún asterisco, es decir, todas nuestras células mueren desde el primer pulso. Lo que ocurre es que nuestro tablero se va actualizando durante la propia aplicación de las reglas.
Recordemos que Python ejecuta el código de arriba hacía abajo, de tal manera, cuando nos enfocamos en la célula tablero[4][5]
, en ese momento, ésta sólo tiene un vecino por lo que muere por regla de aislamiento
Luego, cuando estemos realizando la ejecución en la siguiente fila, la célula que se supone debía nacer de acuerdo al patrón (tablero[5][4]
) permanece muerta pues en ese momento sólo tiene dos vecinos; de forma análoga con la célula tablero[5][6]
que en ese momento tendrá sólo un vecino
y dado que la célula tablero[6][5]
es la única sobreviviente, por regla de aislamiento morirá. Es así como nuestro tablero, desde el primer pulso, queda con todas las células muertas y por ende en los consecuentes pulsos permanece así.
Para solucionar el problema debemos de auxiliarnos de otro tablero (denominémosle tablero2
) para ir almacenando los cambios. Esto es, almacenaremos el número de vecinos de cada célula y aplicaremos las reglas de supervivencia al tablero2
. Por ejemplo, la célula tablero[4][5]
tiene sólo un vecino, la célula tablero[5][4]
tiene 3 vecinos, la célula tablero[5][6]
también tiene 3 vecinos, la célula tablero[5][5]
tiene dos vecinos y la célula tablero[5][6]
sólo tiene un vecino. Por ende, tendremos ya almacenados el número total de vecinos de cada célula. Después, en el tablero2
al aplicar las reglas tendremos que la célula tablero[4][5]
muere por tener sólo un vecino; las células tablero[5][4]
y tablero[5][6]
nacen al tener 3 células vecinas; la célula tablero[5][5]
permanece viva por tener 2 vecinos y la célula tablero[5][6]
muere por tener almacenado sólo un vecino.
Mientras que los cambios que tendremos en el tablero2
son
lo cual ya nos arroja el comportamiento que esperabamos. Para conseguir lo anterior en código escribimos
# creamos un nuevo tablero en cada pulso
for t in range(pulsos):
# Preparamos el tablero nuevo
tablero2 = []
for i in range(filas):
tablero2.append([0] * columnas)
# mostramos los pulsos
# .
# .
# .
# código restante
# .
# .
# .
# aplicamos las reglas en el tablero2
# Supervivencia
if tablero[y][x] and (n == 2 or n ==3):
tablero2[y][x] = True
# Nacimiento
elif not tablero[y][x] and n == 3:
tablero2[y][x] = True
# Superpoblacíon y aislamiento
else:
tablero2[y][x] = False
# Actualizamos el tablero para traer los cambios al tablero original
tablero = nuevo
Al realizar las anteriores modificaciones a nuestro código original conseguiremos el resultado buscado. Finalmente tenemos el código completo:
# dimensiones de la matriz
filas = 10
columnas = 10
# Creamos una lista vacía
tablero = []
# agregamos el valor de False en todas las entradas
for i in range(filas):
tablero.append ([False] * columnas)
# asignaremos el valor de True a las siguientes entradas para dibujar
# el patrón anterior (patrón inicial)
tablero[4][5] = True
tablero[5][5] = True
tablero[6][5] = True
# Tablero y pulsos
pulsos = 6
for t in range(pulsos):
# Preparamos un nuevo tablero
tablero2 = []
for i in range(filas):
tablero2.append([0] * columnas)
# mostramos los pulsos
print("Pulso: ", t + 1)
# Actualizamos el tablero
for y in range(filas):
for x in range(columnas):
# número de vecinos
n = 0
if y > 0 and x > 0 and tablero[y-1][x-1]:
n += 1
if x > 0 and tablero[y][x-1]:
n += 1
if y < filas - 1 and x > 0 and tablero[y+1][x-1]:
n += 1
if y > 0 and tablero[y-1][x]:
n += 1
if y < filas - 1 and tablero[y+1][x]:
n += 1
if y > 0 and x < columnas - 1 and tablero[y-1][x+1]:
n += 1
if x < columnas - 1 and tablero[y][x+1]:
n += 1
if y < filas - 1 and x < columnas - 1 and tablero[y+1][x+1]:
n += 1
# Supervivencia
if tablero[y][x] and (n == 2 or n ==3):
tablero2[y][x] = True
# Nacimiento
elif not tablero[y][x] and n == 3:
tablero2[y][x] = True
# Superpoblacíon y aislamiento
else:
tablero2[y][x] = False
# Actualizamos el tablero
tablero = tablero2
# mostramos el tablero
for y in range(filas):
for x in range(columnas):
if tablero[y][x]:
print("*", end = " ")
else:
print(".", end = " ")
print()
Pulso: 1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . * * * . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Pulso: 2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . * . . . . . . . . . * . . . . . . . . . * . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Pulso: 3 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . * * * . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Pulso: 4 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . * . . . . . . . . . * . . . . . . . . . * . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Pulso: 5 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . * * * . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Pulso: 6 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . * . . . . . . . . . * . . . . . . . . . * . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .