💾 Archived View for tilde.team › ~desertmouse › postdir › abb4eee1bbf316719c5e047e354a742ab000e6a0.g… captured on 2023-01-29 at 17:58:27. Gemini links have been rewritten to link to archived content
⬅️ Previous capture (2023-01-29)
-=-=-=-=-=-=-
______ ______ ______ /\_____\ /\_____\ /\_____\ ____ _\ \__/_/_ \ \__/_/_ \ \__/_/ /\___\ /\_\ \_____\ /\ \_____\ /\ \___\ /\ \___\ \ \ \/ / / / \ \/ / / / \ \/ / / \ \/ / / \ \/ /\/ / \/_/\/ / \/_/_/ \/_/_/ \/_/\/_/ \/_/
So this is something I assume nobody in the tildeverse wants to learn about, but that way my gemlog would remain empty :D
I made Conway's Game Of Life on my Raspberry Pi Pico using MicroPython!
The project uses a pi pico and a 128x64 SSD1306 based OLED display module. It is an I2C based module so communication with it is just two pins, plus an extra two for power. However, using I2C also poses a limitation in speed with the current state of my project (I think). I do have some fixes in mind, and it should be pretty simple.
So I'll just go on to explain how my program works, and the path I took to reach its current, incredibly slow version.
If you don't know about the game of life, it is a cellular automaton devised by the British mathematician John Horton Conway in 1970. It is a zero-player game, so the player only interacts with the game by creating an initial configuration of cells, and four rules guide the cells' evolution from that point.
There's an interactive mode where the user can input co-ordinates manually or load presets using a USB serial terminal like minicomp. But the user won't be carrying around their computer for that and I haven't added any buttons too for now. So, by default I turn the interactive mode off.
Fun fact: I set up a pretty cool drawing system that uses only three buttons - right, left and the middle one that toggles the right/left buttons to behave as down/up buttons. It works, but buttons don't register sometimes, and the circuit too becomes a mess with that. So for the controls I will either use an arduino with a gamepad shield that I have lying around or I will use an old TV remote by decoding its IR signals.
So first, the screen is filled randomly with pixels. A dictionary called 'alive' is initialized. The user can edit the program to set how many generations they want the game of life to simulate.
The program then enters a while loop that will break when the specified number of generations has been simulated.
Before I move forward, I need to explain the rules Game Of Life follows:
- Any live cell with fewer than two live neighbours dies, as if by underpopulation.
- Any live cell with two or three live neighbours lives on to the next generation.
- Any live cell with more than three live neighbours dies, as if by overpopulation.
- Any dead cell with exactly three live neighbours becomes a live cell, as if by reproduction.
These rules can be further compressed into the following:
- Any live cell with two or three live neighbours survives.
- Any dead cell with three live neighbours becomes a live cell.
- All other live cells die in the next generation. Similarly, all other dead cells stay dead.
So, to take care of this, the program loop uses a function 'countLiveNeighbours()' that returns the number of cells that are alive in the eight neighbours in the grid.
This is done for each pixel, and just the cells that will live are appended to the {alive} dictionary. We need to only take care of alive cells because when the program moves on to the next generation, the screen is cleared and that takes care of dead cells.
Fun fact: I first forgot to include the diagonal neighbours, and 'countLiveNeighbours()' only checked four out of the eight neighbours it was supposed to check. This led to the following type of outputs that are more clustered together and look like terrain maps:
https://tilde.zone/@mouse/108752250218060779
I was first actually getting too comfortable with MicroPython and just appended coordinate tuples to an {alive} *list*. That caused a MemoryError (yay!).
It was after that, that I started using a dictionary instead of a plain list. Inside {alive}, coordinates are stored like this:
alive = { 0 : [1,3,5,7], 1 : [2,5,8,45], ... }
Since there are 64 y-coordinates and 128 x-coordinates possible, the keys are the y-coordinate and the numbers in the value list are x-coordinates.
This didn't contain so much of the duplication that was inside my old approach due to the tuples. As a result, I no longer got a MemoryError.
To process all neighbours, the program enters an endless while loop inside the main loop which it exits when the y-coordinate of the 'cursor_y' variable becomes greater than the display's y-limit (127).
So after all of the pixel neighbours have been processed using 'countLiveNeighbours()' 128*64=8192 times along with adding to {alive} according to the game rules, the 'nextGeneration()' function is called, which resets the display, activates all of the pixels in {alive}, and clears {alive}. The counting variable for keeping track of generations is incremented, and the program moves forward.
The process is repeated to give you Conway's Game Of Life.
The onboard LED on the pico also blinks when the next generation is rendered. When the program ends, it quickly blinks two times to signal that.
Now, I am adding a placeholder here that I will update with a video of the running program. Since I am using the ssd1306 library's display.pixel(x,y) method to retrieve the value of a pixel, the program is quite slow. (I am basically storing the map on the display itself.)
< video link goes here >
I am pretty new to micropython and still exploring it by building projects like this, so I got scared when I got the memory error I was talking about earlier.
In a future version, I want to make it run way faster. Here is what I will do:
Similar to the {alive} dictionary, I will make a 'board' dictionary, along with a manager function that either 'get's or 'set's values on the board. I think it will be a lot faster than what I currently have.
Everything else will remain the same. I might also make use of the second core to make it even faster!
I couldn't make much use of the second core in this first iteration because of both my inexperience and the problems that could arise if both cores access I2C at the same time. I am currently learning about multicore programming using micropython and have made only two very basic programs that make use of it. I am expecting much higher speeds by having the board in memory so I don't think I will even need to incorporate the second core in this particular project.
Another method that will be easier and will kind of complete version 1 for me, is making each cell 2x2.
I think I'll just have to set the display's limits to 63,31 and everything else should adjust. The only other thing to be done would be modify the rendering function to draw 2x2 square, which is very easy.
I will end version 1 of this project with this and then do the memory board in version 2.
Thank you for reading. I hope you enjoyed this.
As I learn more, I want to document projects like this, so please, if you have any quesitons about the project or suggestions, feel free to hit me up on the tildeverse IRC at 'desertmouse'.
~desertmouse