The 2020 Summer Chaos Games (in 2021)
Chaos was the law of nature; Order was the dream of man.
—Henry Adams
Recently I watched a Youtube video from Numberphile that piqued my interest. If you aren’t familiar with the channel, they produce a lot of short math videos with the rest of us in mind. This one was about chaos games. I encourage everyone to watch it.
You can see several more complex examples on the Wikipedia page. I’ll recreate a few of them at the end of this post.
If you can’t watch the video I’ll do my best to summarize chaos games, although I don’t claim to be a mathematician:
Start by marking n corners of a polygon—maybe 3, 4, or 5—and pick a random point inside the shape. I’ll call this the “cursor.” Select one of the corners at random and move the cursor halfway toward it. Draw a point at the new cursor position. Then select another corner and draw another point. Continue this random process thousands of times until gradually a striking, seemingly non-random pattern emerges.
The video demonstrates a triangle with purely random movement—probably the easiest game to demonstrate. However at one point, as the presenter is drawing points by hand, he says, “It’s possible that something interesting would happen if I could do this quick enough… Maybe we want a computer to do this.” I couldn’t agree more.
Let’s automate the process with Python. I’ll implement a Cursor
class to keep track of the points.
class Cursor: def __init__(self, starting_x, starting_y): self.x = starting_x self.y = starting_y self.movement_history = [(self.x, self.y)] def move(self, new_coords): new_x, new_y = new_coords self.x += (new_x - self.x) / 2 self.y += (new_y - self.y) / 2 self.movement_history.append((self.x, self.y))
Vertices (corners) are placed as seen below. Note that the shape doesn’t have to be a regular polygon. In fact I stretched it here to make the code less complicated. But getting close will make the output easier to recognize.
The initial position is set at Vertex #1. But as I mentioned above, it doesn’t have to be placed here. Any random point inside the triangle will do.
cursor = Cursor(starting_x=0, starting_y=0)
The engine driving this simulation is a loop that selects a random vertex and passes its coordinates to the cursor.move
method.
vertex_dict = {1: (0, 0), 2: (10, 0), 3: (5, 8)} for _ in range(10000): r = randint(1, 3) cursor.move(vertex_dict[r])
The choice of ten thousand iterations is entirely arbitrary. Play with this number and see how your output changes. Or better yet, take several snapshots and watch the pattern emerge.
After recording thousands of coordinates, let’s plot the information stored in cursor.movement_history
. All we need is a bare-bones Matplotlib plot.
x, y = zip(*cursor.movement_history) fig, ax = plt.subplots() ax.scatter(x, y, s=1) ax.set_xticks(range(11)) ax.set_yticks(range(9)) ax.set_xlim(-1, 11) ax.set_ylim(-0.8, 8.8) fig.tight_layout() plt.show()
And the result:
Sure enough, it looks like something! You might recognize this pattern from a poster hanging on your high school math teacher’s wall. It’s known as the Sierpiński triangle.
But the chaos games are far from over. You can add more vertices and additional rules to create more complex fractals.
For example, lay out a four-sided polygon and include a rule that a vertex cannot be chosen twice in a row. You’ll end up with something like this:
Or a rule that a chosen vertex can’t be diagonal from the previous one:
Step up to five-sided polygons and you can create even more complex patterns. Here’s a pentagon whose vertices can’t be selected twice in a row:
Although at this point my low-resolution Matplotlib plots don’t really do justice to the chaos games.
I find it fascinating that these random—chaotic—algorithms can create such ordered designs.
What would happen if you jumped some distance other than halfway? Experiment with your own rules and see what you can create.
Full code:
from random import randint import matplotlib.pyplot as plt class Cursor: def __init__(self, starting_x, starting_y): self.x = starting_x self.y = starting_y self.movement_history = [(self.x, self.y)] def move(self, new_coords): new_x, new_y = new_coords self.x += (new_x - self.x) / 2 self.y += (new_y - self.y) / 2 self.movement_history.append((self.x, self.y)) cursor = Cursor(0, 0) vertex_dict = {1: (0, 0), 2: (10, 0), 3: (5, 8)} for _ in range(10000): r = randint(1, 3) cursor.move(vertex_dict[r]) x, y = zip(*cursor.movement_history) fig, ax = plt.subplots() ax.scatter(x, y, s=1) ax.set_xticks(range(11)) ax.set_yticks(range(9)) ax.set_xlim(-1, 11) ax.set_ylim(-0.8, 8.8) fig.tight_layout() plt.show()