You explain that you wish to improve CPU consumption but you
don't know where to begin.
Always start optimizing by asking "where did the cycles go?" Run the profiler:
$ python -m cProfile -s tottime life.py
Also, a numba @jit on add_life_to_matrix wouldn't hurt.
You are using list-of-lists, which is very pythonic, but it involves lots of random reads for object pointer chasing. Consider using a cache-friendly array instead, or perhaps an ndarray.
As with any project, definitely record timing measurements before and after a refactor, so you're sure it had the desired effect.
lg = LifeGame() lg.spawn_life()
Looks great!
iteration = 0 while True:
Consider pushing iteration down into the game object.
You compute a very nice coords data structure. But it feels like that computation should happen within the game class, perhaps as a local variable of an .update() method.
lg.print(iteration) time.sleep(0.010) os.system('cls')
I imagine there's a lot of flicker associated with that.
Rather than a heavyweight clearscreen approach, you could just send an erase command: <esc>[2J
Better, you could position the cursor at top-of-screen and rewrite cells over displayed cells, to reduce flicker.
Or consider using pygame pixels to represent live cells.
In spawn_life you jump through a bunch of hoops to create a temporary data structure and then consume it. It's unclear why we'd want to create it in the first place. Just pick a random (x, y) and place a new * star there.
for i in range(1):
That's an odd way to say "do the following exactly once."
I assume it's leftover from debug, and that 1 is a magic number that you sometimes change. Bury that constant in the signature instead:
def spawn_life(self, num_births=20): ... for _ in range(num_births): x, y = randint... self.matrix[y][x] = "*"
No need for a return value that will just be ignored.
I tend to think of get_available_coords as "get neighbor coords", but whatever, that works fine.
available_coords = [[x-1, y], [x+1, y], [x, y+1], [x, y-1]]
Rather than list of lists, the pythonic way to phrase that would be list of 2-tuples:
available_coords = [(x-1, y), (x+1, y), (x, y+1), (x, y-1)]
We use list for things that are all "the same", and tuple for things with fixed number of elements where element position affects the meaning. Think of it as an immutable C struct.
Wow, that if is quite the mouthful. First let's use black to make it legible:
if ( available_coords[i][0] <= self.width - 1 and available_coords[i][0] >= 0 and available_coords[i][1] <= self.height - 1 and available_coords[i][1] >= 0 and self.matrix[available_coords[i][1]][available_coords[i][0]] == " " ): coords.append([available_coords[i][0], available_coords[i][1]])
That available_coords[i] expression is tediously long and we use it several times, so let's assign temp vars:
x, y = available_coords[i]
Notice that element [0] went into x and [1] into y.
Now let's do some per-axis conjunct chaining:
if ( 0 <= x < self.width and 0 <= y < self.height and self.matrix[y][x] == " " ):
In add_life_to_matrix the combination of these two
chance = random.randint(0, 100) if chance == 1:
amounts to a Magic Number of .01. Better to put it in the method signature, so unit tests or other callers can modify it when desired.
You asked about making this go faster, presumably for a larger universe than 20 x 20 cells. In 1970 Martin Gardner wrote about the game Conway devised, and a rich literature has sprung up around it since. It includes sightings of Gliders and of larger beasts, descriptions of how to find and construct such beasts, and algorithms to efficiently compute the next generation or the K-th subsequent generation.
Consider a canvas of more than 1000 x 1000 cells, with a glider gun near the origin that keeps pumping out new gliders. Out of a million cells, most will be dead in this generation, and in every subsequent generation, so they need not be recomputed. Even if the original setup arranged for some isolated beehives to be present, they won't change either. Segmenting the universe into interesting and unchanging sections is key to efficiently computing large Life scenarios. Typically we only refresh the display every K iterations, so even an isolated blinker or other cyclic beast may be ignored for a while, until we are at last asked to render its neighborhood in the K-th generation.
This code achieves many of its design goals. Some of the responsibilities currently shouldered by callers could sensibly be pushed down into the game class.
I would be happy to delegate or accept maintenance tasks for this codebase. There are some recommended code cleanups that really ought to happen before merging to main.