El juego de la vida es un autómata celular diseñado por el matemático John Horton Conway en 1970. Es un juego con el que muchos nos hemos encontrado en nuestra formación o curioseando con juegos matemáticos por simple diversión.
La tesis es muy sencilla: tenemos un microuniverso formado una matriz, donde cada celda identifica a una célula en dos posibles estados «Viva» o «Muerta» (dejaremos a Schrödinger y su gato para otros juegos ).
La evolución de nuestro sistema se define por dos sencillas reglas:
- Un celda con una célula muerta puede volver a la vida si tiene exactamente 3 vecinas vivas
- Una celda con una célula viva morirá si se encuentra demasiado sola (menos de 2 vecinas vivas) o existe superpoblación (más de 3 vecinas vivas)
Con estas reglas y un estado inicial de algunas células vivas distribuidas y agrupadas por la matriz, tendremos un universo que evoluciona y donde la vida se extiende y se extingue. Casi siempre llegamos a estados «Estables» donde la distribución se queda fija, ya sea en forma de estructuras fijas o que se repiten en pocos ciclos o bien por que se produce una extinción total y nos quedamos sin vida en nuestro microuniverso.
Recientemente varios canales de Youtube, que sigo asiduamente (Derivando y Dot CSV), han abordado el «Juego de la vida» y me he animado a llevarlo a la práctica en Python.
Con apenas 100 líneas de código disponemos de la posibilidad de ser dioses en nuestros pequeños y acotados microuniversos.
Implementación del Juego de la Vida en Python
Librerias usadas
En primer lugar necesitamos contar con las librerias que nos van a permitir desarrollar el juego. Estas son:
- pygame: para implementar la pantalla de representación y la interación con el teclado y ratón
- numpy: para contar con estructuras efcicientes de matrices y sus operaciones
- time: para incluir funciones de control temporal
Si no dispones de estas librerías puedes instalarlas mediante pip.
> pip install pygame > pip install numpy
import pygame import numpy as np import time
Inicialización de nuestro «microuniverso»
En primer lugar necesitamos inicializar nuestra ventana de presentación con el tamaño que deseemos (en este caso 400×400 píxeles) color de fondo gris oscuro (25,25,25).
pygame.init() width, height = 400, 400 bg = 25, 25, 25 screen = pygame.display.set_mode((height, width)) screen.fill(bg)
A continuación definimos la matriz de celdas que representará nuestro microuniverso con un tamaño de 60×60 (por ejemplo). Partiremos de una matriz de ceros de dicho tamaño, que representará la ausencia de vida de partida, y calculamos el tamaño que tendrá cada celda.
# Tamaño de nuestra matriz nxC, nyC = 60, 60 # Estado de las celdas. Viva = 1 / Muerta = 0 gameState = np.zeros((nxC, nyC)) #dimensiones de cada celda individual dimCW = width / nxC dimCH = height / nyC
Para iniciar nuestro sistema con algo de vida, seleccionaremos algunas celdas como 1 («viva»).
Existen agrupaciones interesantes de células con un compartamiento bien identificado y que podemos distribuir por nuestra matriz. Cada una de estas estructuras tienen caractísticas conocidas y bien clasificadas como:
- Caja 2×2: una estructura invariable en el tiempo (vidas estáticas)
- Línea 3×1: una estructura que rota en 45º en cada iteración (osciladores)
- El corredor: una estructura de 5 celdas vivas que se desplaza en diagonal por la matriz (naves espaciales).
- La serpiente, la rana … y mil formas que puedes probar
De la interacción de ellas aparecerán nuevas estructuras que irán evolucionando infinitamente o hasta llegar a estados estables donde la esta se detiene.
A continuación incluimos algunos de estos elementos en nuestra matriz
# Oscilador. gameState[38, 20] = 1 gameState[39, 20] = 1 gameState[40, 20] = 1# Oscilador. gameState[38, 20] = 1 gameState[39, 20] = 1 gameState[40, 20] = 1 # Runner 1 gameState[10,5] = 1 gameState[12,5] = 1 gameState[11,6] = 1 gameState[12,6] = 1 gameState[11,7] = 1 #Runner 2 gameState[5,10] = 1 gameState[5,12] = 1 gameState[6,11] = 1 gameState[6,12] = 1 gameState[7,11] = 1 #Box 1 gameState[18,15] = 1 gameState[17,16] = 1 gameState[17,15] = 1 gameState[18,16] = 1 #Serpent 1 gameState[30,20] = 1 gameState[31,20] = 1 gameState[32,20] = 1 gameState[32,19] = 1 gameState[33,19] = 1 gameState[34,19] = 1
Antes de entrar en el bucle que dará lugar a cada iteración de nuestro sistema, definiremos la variable que nos permitirá controlar desde nuestro teclado la ejecución o pausa de nuestro sistema. Iniciaremos nuestro sistema en sin pausa
pauseExect = False
Bucle de iteración
Dentro de nuestro bucle se incluirán las reglas que debe seguir nuestro sistema en cada iteración (las dos reglas indicadas por Conway), algunas reglas de control para interactuar con el teclado y el ratón (pausa y play, así como inserción de células vivas cliqueando con el ratón en las posiciones elegidas de la matriz) y el repintado de la matriz tras cada cambio de estado.
# Bucle de ejecución while True: # Copiamos la matriz del estado anterior # #para representar la matriz en el nuevo estado newGameState = np.copy(gameState) # Ralentizamos la ejecución a 0.1 segundos time.sleep(0.1) # Limpiamos la pantalla screen.fill(bg)
Registramos los eventos de teclado y ratón para interactuar con el sistema. La pulsación de una tecla del teclado producirá la pausa o activación de los cambios de estados, mientras que la pulsación de teclas de ratón revivirán las células muertas, que nos permitirá incluir nuevas estructuras vivas a lo largo de la ejecución.
# Registramos eventos de teclado y ratón. ev = pygame.event.get() for event in ev: # Detectamos si se presiona una tecla. if event.type == pygame.KEYDOWN: pauseExect = not pauseExect # Detectamos si se presiona el ratón. mouseClick = pygame.mouse.get_pressed() if sum(mouseClick) > 0: posX, posY = pygame.mouse.get_pos() celX, celY = int(np.floor(posX / dimCW)), int(np.floor(posY / dimCH)) newGameState[celX, celY] = 1
A continuación recorremos todas las celdas de la matriz, en el eje Y y en el eje X, para contabilizar cuantos vecinos vivos tiene cada celda.
Un caso particular son aquellas celdas que forman parte del borde, que en principio no disponen de todos los vecinos. Una solución a esto es considerar que las filas del borde tienen como vecinas a las correspondientes del borde opuesto. De esta forma consideramos que la matriz representa la superficie de un toroide y que por tanto existe continuidad en los bordes.
Esto los conseguimos mediante la función módulo (%) con respecto al número de casillas de cada fila (nxC) o columna (nyC).
for y in range(0, nxC): for x in range (0, nyC): if not pauseExect: # Calculamos el número de vecinos cercanos. n_neigh = gameState[(x - 1) % nxC, (y - 1) % nyC] + \ gameState[(x) % nxC, (y - 1) % nyC] + \ gameState[(x + 1) % nxC, (y - 1) % nyC] + \ gameState[(x - 1) % nxC, (y) % nyC] + \ gameState[(x + 1) % nxC, (y) % nyC] + \ gameState[(x - 1) % nxC, (y + 1) % nyC] + \ gameState[(x) % nxC, (y + 1) % nyC] + \ gameState[(x + 1) % nxC, (y + 1) % nyC] # Regla #1 : Una celda muerta con exactamente 3 vecinas vivas, "revive". if gameState[x, y] == 0 and n_neigh == 3: newGameState[x, y] = 1 # Regla #2 : Una celda viva con menos de 2 o 3 vecinas vinas, "muere". elif gameState[x, y] == 1 and (n_neigh < 2 or n_neigh > 3): newGameState[x, y] = 0
Tras conocer el número de vecinos vivos de cada celda procedemos a aplicar las dos reglas mencionadas, y pintamos el resultado según el resultado se «Viva» o «Muerta».
# Calculamos el polígono que forma la celda. poly = [((x) * dimCW, y * dimCH), ((x+1) * dimCW, y * dimCH), ((x+1) * dimCW, (y+1) * dimCH), ((x) * dimCW, (y+1) * dimCH)] # Si la celda está "muerta" pintamos un recuadro con borde gris if newGameState[x, y] == 0: pygame.draw.polygon(screen, (40, 40, 40), poly, 1) # Si la celda está "viva" pintamos un recuadro relleno de color else: pygame.draw.polygon(screen, (200, 100, 100), poly, 0)
Por último actualizamos el estado de la matriz y mostramos el resultado en pantalla
# Actualizamos el estado del juego. gameState = np.copy(newGameState) # Mostramos el resultado pygame.display.flip()
El script completo lo puedes descargar aquí. Ahora sólo queda ejecutarlo y probar a incluir nuevas estructuras y a jugar a ser «dios».