Skip to content

Instantly share code, notes, and snippets.

@thinkyhead
Created January 29, 2025 05:33
Show Gist options
  • Save thinkyhead/7065c70f0e5920a3e915a1ad35b081bd to your computer and use it in GitHub Desktop.
Save thinkyhead/7065c70f0e5920a3e915a1ad35b081bd to your computer and use it in GitHub Desktop.
#!/usr/bin/env python
#
# Conway's Game of Life (Three Color Variant)
# Author: @thinkyhead
# Date: 11 Oct 2024
#
# P = PAUSE
# Q = QUIT
# K = CLEAR
# B = SPAWN BLINKERS
# G = SPAWN GLIDERS
# H = SPAWN GLIDER GUNS
# L = SPAWN LINES
# X = SPAWN EXPLODERS
#
import pygame
import random
import time
# Milliseconds to delay between each update
DELAY = 50
# Set screen dimensions
WIDTH = 1024
HEIGHT = 768
screen : pygame.Surface = None
# Cell size
CELL_SIZE = 4
# Colors
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
RED = (255, 0, 0)
GREEN = (0, 255, 0)
YELLOW = (255, 224, 0)
color_by_type = (BLACK, GREEN, YELLOW, RED)
colors = len(color_by_type) - 1
# Create a grid to store the cells
WW = WIDTH // CELL_SIZE
HH = HEIGHT // CELL_SIZE
grid = [[0 for _ in range(WW)] for _ in range(HH)]
population = 0
prevpop = [ 0, 0 ]
stable_count = 0
grow_count = 0
# Function to draw the grid
def draw_grid():
screen.fill(BLACK)
for r in range(HH):
for c in range(WW):
pygame.draw.rect(screen, color_by_type[grid[r][c]], (c * CELL_SIZE, r * CELL_SIZE, CELL_SIZE, CELL_SIZE))
# Update the display
pygame.display.flip()
# Function to count the number of living neighbors
def count_neighbors(row, col):
around = [(-1, -1), (0, -1), (1, -1), (-1, 0), (1, 0), (-1, 1), (0, 1), (1, 1)]
neighbors = [0] * colors
for dx, dy in around:
r, c = (row + dx), (col + dy)
if (0 <= r < HH) and (0 <= c < WW) and (grid[r][c] > 0):
neighbors[grid[r][c] - 1] += 1
return neighbors
# Function to update the grid based on Conway's rules
def update_grid():
global grid, population, prevpop, stable_count, grow_count
# Create a new nested array 'new_grid' as a copy of 'grid'
new_grid = [[0 for _ in range(WW)] for _ in range(HH)]
population = 0
for r in range(HH):
for c in range(WW):
n = count_neighbors(r, c)
total = n[0] + n[1] + (n[2] if colors == 3 else 0)
# The original cell's value
v = grid[r][c]
# Rule 1: Any live cell with fewer than two live neighbors dies (underpopulation).
# Rule 2: Any live cell with two or three live neighbors lives on to the next generation.
# Rule 3: Any live cell with more than three live neighbors dies (overpopulation).
# Rule 4: Any empty cell with exactly three live neighbors becomes a live cell (reproduction).
if v == 0:
if total == 3:
if colors == 3:
v = 3 if n[2] > 1 else 2 if n[1] > 1 else 1 if n[0] > 1 else random.randint(1, 3)
else:
v = 2 if n[1] > 1 else 1
elif not (2 <= total <= 3):
v = 0
new_grid[r][c] = v
population += (v > 0)
grid = new_grid
# If the population is the same as two generations ago, we might have reached a stable state
if prevpop[0] == population:
stable_count += 1
grow_count = 0
else:
stable_count = 0
grow_count += (1 if prevpop[0] < population else -1)
# Remember this population, retain the previous one
prevpop[0] = prevpop[1] ; prevpop[1] = population
# Function to handle mouse clicks to set cells
def handle_click(pos, color):
row = pos[1] // CELL_SIZE
col = pos[0] // CELL_SIZE
if 0 <= row < HH and 0 <= col < WW:
grid[row][col] = color
# Function to initialize the grid with random cells
def init_grid():
for r in range(HH):
for c in range(WW):
# Randomly set cell state to alive (1,2,...) or dead (0)
if (random.random() < 0.2):
grid[r][c] = random.randint(1, colors)
def spawn_group(row, col, rot, group):
if rot < 0:
rot = random.randint(0, len(group) - 1)
gy = len(group[rot])
gx = len(group[rot][0])
if row < 0:
row = random.randint(0, HH - gy - 1)
if col < 0:
col = random.randint(0, WW - gx - 1)
for r in range(gy):
for c in range(gx):
if group[rot][r][c]:
grid[row + r][col + c] = random.randint(1, colors)
def spawn_mutated_group(row, col, rot, group):
hbase = [list(reversed(row)) for row in group]
vbase = [row for row in reversed(group)]
hvbase = [list(reversed(row)) for row in vbase]
rotated = [list(row) for row in zip(*group)]
hrot = [list(reversed(row)) for row in rotated]
vrot = [row for row in reversed(rotated)]
hvrot = [list(reversed(row)) for row in vrot]
spawn_group(row, col, rot, [group, hbase, vbase, hvbase, rotated, hrot, vrot, hvrot])
def spawn_xploder(row=-1, col=-1, rot=-1):
xploder = (
[0, 1, 0, 0],
[0, 1, 1, 0],
[1, 0, 0, 1],
[0, 1, 1, 0]
)
spawn_mutated_group(row, col, rot, xploder)
def spawn_blinker(row=-1, col=-1, rot=-1):
blinker = (
[0, 0, 0],
[1, 1, 1],
[0, 0, 0]
)
spawn_mutated_group(row, col, rot, blinker)
def spawn_glider(row=-1, col=-1, rot=-1):
glider = (
[0, 1, 0],
[0, 0, 1],
[1, 1, 1]
)
spawn_mutated_group(row, col, rot, glider)
def spawn_glider_gun(row=-1, col=-1, rot=-1):
glider_gun = (
(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1),
(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1),
(1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
(1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
)
spawn_mutated_group(row, col, rot, glider_gun)
def spawn_line():
if random.random() < 0.5:
# Vertical line in a single column
len = random.randint(6, HH // 2)
row = random.randint(2, HH - 2 - len)
col = random.randint(2, WW - 2)
for r in range(row, row + len - 1):
grid[r][col] = random.randint(1, colors)
else:
# Horizontal line in a single row
len = random.randint(6, WW // 2)
col = random.randint(2, WW - 2 - len)
row = random.randint(2, HH - 2)
for c in range(col, col + len - 1):
grid[row][c] = random.randint(1, colors)
def main():
global screen, running, paused, drawing, color, pos
global population, prevpop, stable_count, grow_count
global grid
# Initialize PyGame
pygame.init()
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Conway's Game of Life (Color Variant)")
# Initialize the grid with random cells at the start of the game
init_grid()
update_grid()
update_grid()
# Main game loop
running = True
paused = False
drawing = False
color = 2
pos = None
while running:
for event in pygame.event.get():
# QUIT
if event.type == pygame.QUIT:
running = False
# MOUSE BUTTON CLICK
elif event.type == pygame.MOUSEBUTTONDOWN:
drawing = True
color = color + 1 if color < colors else 1
handle_click(event.pos, color)
# MOUSE MOVE
elif event.type == pygame.MOUSEMOTION:
if drawing and (not (pygame.key.get_mods() & pygame.KMOD_META)):
handle_click(event.pos, color)
# MOUSE BUTTON RELEASE
elif event.type == pygame.MOUSEBUTTONUP:
# Check for SHIFT key
if not (pygame.key.get_mods() & pygame.KMOD_META):
drawing = False
# KEY PRESS
elif event.type == pygame.KEYDOWN:
# P = PAUSE
# Q = QUIT
# K = CLEAR
# B = SPAWN BLINKERS
# G = SPAWN GLIDERS
# H = SPAWN GLIDER GUNS
# L = SPAWN LINES
# X = SPAWN EXPLODERS
if event.key == pygame.K_p:
paused = not paused
elif event.key == pygame.K_q:
running = False
elif event.key == pygame.K_k:
grid = [[0 for _ in range(WW)] for _ in range(HH)]
population = 0
elif event.key == pygame.K_b:
for r in range(10): spawn_blinker()
elif event.key == pygame.K_g:
for r in range(10): spawn_glider()
elif event.key == pygame.K_h:
for r in range(1): spawn_glider_gun()
#paused = True
elif event.key == pygame.K_l:
for r in range(4): spawn_line()
elif event.key == pygame.K_x:
for r in range(10): spawn_xploder()
if drawing: draw_grid() ; continue
# Get the current time in milliseconds
t = pygame.time.get_ticks()
# Update the grid, using up some time
if paused:
time.sleep(0.1)
continue
update_grid()
draw_grid()
# Print the population at the top of the screen
if stable_count > 1000: xtra = "Superstable"
elif stable_count > 10: xtra = "Stable"
elif grow_count > 10: xtra = "Growing"
elif grow_count < -10: xtra = "Reducing"
else: xtra = 'Dynamic'
pygame.display.set_caption(f"Conway's Game of Life (Color Variant) - Population: {population} - {xtra}")
#pygame.time.delay(DELAY - (pygame.time.get_ticks() - t))
# Quit PyGame
pygame.quit()
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment