Curso de introducción a la programación con Python

Autor: Luis Fernando Apáez Álvarez

El juego de la vida

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:

  1. Regla de nacimiento: Una célula muerta resucita si tiene exactamente 3 vecinos vivos (representaremos a las células vivas de color negro y a las muertas de color blanco). Por ejemplo, en los siguientes tres casos las células muertas (marcadas de gris) resucitarán en el siguiente tiempo o pulso

Captura1111.PNG

  1. Regla de la supervivencia: Una célula permanecerá viva si tiene dos o tres vecinos. En los siguientes tres casos las células vivas (marcadas con un pequeño cuadrado blanco) permanecerán vivas en el siguiente pulso.

Captura11112.PNG

  1. Regla de la súperpoblación: Una célula muere o permanece muerta si tiene cuatro o más vecinos. En los siguientes tres casos las células marcadas con un pequeño cuadrado gris estarán muertas o permanecerán muertas en el siguiente pulso

Captura11113.PNG

  1. Regla de aislamiento: Una célula muere o permanece muerta si tiene menos de dos vecinos. En los siguientes tres casos las células marcadas con un pequeño cuadrado gris estarán muertas o permanecerán muertas en el siguiente pulso

Captura11114.PNG

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

Captura11115.PNG

Teniendo esta forma inicial el juego de la vida tomará un comportamiento cíclico

Captura11116.PNG

Este tipo de patrones se conocen como oscilatorios y en general tenemos los siguientes tipos:

  1. 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.

  2. 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.

  3. Naves espaciales: Son patrones que aparecen en otra posición tras completar su período.

  4. 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

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

Captura11115.PNG

lo cual conseguiremos mediante el código

Luego, es preciso aplicar los pulsos para que el patrón cambie, para ello bastará con implementar un bucle for que englobe nuestro tablero

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

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:

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

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

Captura3.PNG

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

Captura4.PNG

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.

Captura5.PNG

Mientras que los cambios que tendremos en el tablero2 son

Captura6.PNG

lo cual ya nos arroja el comportamiento que esperabamos. Para conseguir lo anterior en código escribimos

Al realizar las anteriores modificaciones a nuestro código original conseguiremos el resultado buscado. Finalmente tenemos el código completo:

Captura11116.PNG

Socialmedia.PNG