vertices <- matrix(c(0, 0, 1, 0, 0.5, sqrt(3) / 2), ncol = 2, byrow = TRUE)
n_points <- 40000
pts <- matrix(0, n_points, 2)
pts[1, ] <- c(0.5, 0.3)
for (i in 2:n_points) {
v <- vertices[sample(1:3, 1), ]
pts[i, ] <- (pts[i - 1, ] + v) / 2
}
df <- as.data.frame(pts[-c(1:100), ]) # drop transient
names(df) <- c("x", "y")
ggplot(df, aes(x, y)) +
stat_bin2d(bins = 150) +
scale_fill_viridis_c(option = "plasma", trans = "log10") +
coord_fixed() +
theme_void() +
theme(legend.position = "none")Randomness and Chaos Games
How Simple Random Choices Produce Structured Patterns
The Chaos Game
The classic chaos game generates the Sierpinski triangle by repeatedly moving halfway toward a randomly chosen vertex of a triangle. Despite pure randomness, a highly ordered fractal appears.
R implementation
p5.js: live chaos game
Other Chaos Games
Square with restricted choices (Barnsley-like)
verts <- matrix(c(0, 0, 1, 0, 1, 1, 0, 1), ncol = 2, byrow = TRUE)
n_points <- 50000
pts <- matrix(0, n_points, 2)
pts[1, ] <- c(0.5, 0.5)
prev <- 1
for (i in 2:n_points) {
choices <- setdiff(1:4, prev)
v_idx <- sample(choices, 1)
v <- verts[v_idx, ]
pts[i, ] <- (pts[i - 1, ] + v) / 2
prev <- v_idx
}
df <- as.data.frame(pts[-c(1:200), ])
names(df) <- c("x", "y")
ggplot(df, aes(x, y)) +
stat_bin2d(bins = 120) +
scale_fill_viridis_c(option = "magma", trans = "sqrt") +
coord_fixed() +
theme_void() +
theme(legend.position = "none")p5.js: variable number of vertices
Key Observation
Randomness alone does not produce symmetry. The rules that restrict which random choices are allowed (or how the next point is computed) inject the symmetry. In the Sierpinski case the three vertices and the “halfway” rule together force the triangular symmetry we see.