The creation is based on a game of life on a hexagonal grid. Instead of birth and death, the neighbor count determines whether to freeze or thaw the current cell. This way symmetric and hexagonal structures are created that for many seeds resemble natural snowflakes. The random seed determines the number of iterations, the freezing rules, and the ASCII symbol (out of a small list of symmetric characters) to use for visualization.
The grid is composed to hold a hexagonal grid, thus every other row (row % 2) has to be shifted to the left and right, respectively. This is especially necessary when counting the number of frozen neighbors in get_frozen_neighbors(), where the col_offset picks the correct neighbors for each cell.
TheSimilarly, upon printing the grid in grid2ascii(), we have to add a space in front of even rows, in order to shift them to the correct position.
The actual growing process starts with an initial frozen cell (set to 1) in the center of a large enough grid (initialized unfrozen, i.e. with zeros), which then updates in every iteration via update_grid().
To not saw off the branch we are sitting on, we have to work with a copy of the grid, thus updated_grid will be populated according to the rules and finally returned.
In each iteration we count the number of frozen neighbors, check whether to freeze or unfreeze the current cell based on the freeze-rule and repeat iterating the grid.
# This script generates a snowflake using a hexagonal grid and cell rules for freezing/thawing cells.
import sys
import numpy as np
ROWS, COLS = 30, 30 # Size of the grid
def get_frozen_neighbors(grid, row, col):
# Return -1 for border cells so that they are not processed by any rules
if row <= 0 or row >= grid.shape[0] - 1 or col <= 0 or col >= grid.shape[1] - 1:
return -1
col_offset = 1 if ((row % 2) == 0) else -1 # Adjust column offset for hexagonal grid
return (
grid[row - 1, col]
+ grid[row - 1, col + col_offset]
+ grid[row, col - 1]
+ grid[row, col + 1]
+ grid[row + 1, col]
+ grid[row + 1, col + col_offset]
)
def update_grid(grid, freeze):
updated_grid = grid.copy()
for row in range(grid.shape[0]):
for col in range(grid.shape[1]):
if get_frozen_neighbors(grid, row, col) in freeze:
updated_grid[row, col] = 1
else:
updated_grid[row, col] = 0
return updated_grid
def grid2ascii(grid, ascii_symbol):
for row_idx, row in enumerate(grid):
if np.all(row == 0):
continue # Skip fully empty rows
line = ''
if row_idx % 2 == 0:
line += ' ' # Offset even rows for hexagonal appearance
line += ' '.join((ascii_symbol if cell == 1 else ' ') for cell in row)
print(line)
print("\n")
if __name__ == "__main__":
if len(sys.argv) > 1:
seed = int(sys.argv[1])
else:
seed = 40
np.random.seed(seed)
# Select symbol and number of iterations based on random seed
ascii_symbol = np.random.choice(['+', 'X', '*', '#'])
iterations = np.random.randint(5, 15)
# Randomly generate rulesetfreezing rule based on the seed
freeze = [1] # Has to be included, otherwise, we would never start growing
for i in range(2, 7):
if np.random.choice([True, False]):
freeze.append(i)
grid = np.zeros((ROWS, COLS), dtype=int)
# Freeze the center cell (We have to start somewhere...)
center_row, center_col = ROWS // 2, COLS // 2
grid[center_row, center_col] = 1
for _ in range(iterations):
grid = update_grid(grid, freeze)
# Finally, print the grid using the selected ASCII symbol
grid2ascii(grid, ascii_symbol)