This is the story of how I created something cool and amusing, using computers, electronics, code, creativity and curiosity. The end result being https://www.multilife.jmercha.dev/
johnmerchant / multilife
Multiplayer Game of Life cellular automata simulation
MultiLife.live
An experiment in realtime cellular automata.
See my DEV Community post for the story behind this.
Basically, an interactive multiplayer digital lava lamp.
MultiLife RGB
MutliLife can rendered to an RGB LED matrix panel using a Raspberry Pi with multilife-rgb.
Dependencies
- Node.js 13
- yarn
- A computer with an operating system.
- Or alternatively, Docker
Design
The frontend is implemented in React, using Redux to manage the client-side state.
The game itself is rendered using a <canvas>
.
Game state is managed server side, and events are pushed live between clients and the server using a websocket
The protocol, models and utility functions are all isomorphic. That is to say, it is code able to be executed on both the server and client side.
Running
-
yarn dev-server
- builds and starts the server -
yarn dev-client
- builds and starts the frontend
Origins
I was talking to a few friends and colleagues recently about Conway's Game of Life. It can be basically explained as follows.
There is a grid of cells with 2 states: alive and dead. On each iteration of the game there are a set of rules which are evaluated on each cell on the grid:
- Live cells with < 3 live neighbors die
- Live cells with > 1 live neighbors live on to the next iteration
- Live cells with > 3 neighbors die
- Dead cells with exactly 3 neighbors become alive
Back in the 2000's, I had created a Game of Life Java Applet that ran in the browser - Applets being long since deprecated and the original source code unfortunately lost to time. I had a lot of fun writing it and showing it off to people.
I started to think to myself, could I do it again in 2019 using my favorite web technologies? Could I performantly render a dynamic grid of cells in JSX? How would the game state be represented and updated?
I ended up going down multiple rabbit holes and tangents and ended up learning a lot!
Experimentation
In the initial iterations of my experiment, I attempted to render the the grid as a sequence of JSX elements. <span>
elements with '⬜' (white box) emoji to represent living cells and '⬛' (black box) to represent dead cells. As those familiar with React may know, this wasn't a great idea: the DOM is excruciatingly slow to update, even with React's reconciliation, it was still updating hundreds of DOM elements on each tick, resulting in an unresponsive experience. e.g. a cell click event would take almost 100ms to update the entire grid.
So, how could I performantly render the game of life grid then? The answer is, with a <canvas>. I used React's useEffect hook to paint the game state on each state update to the canvas element.
Multiplayer
I started to think about where to store and handle the game state and decided to manage the game state in Redux so I could clearly define and handle game events using actions and reducers. To put it as simply as possible, Redux is a "state container" that allows you to reduce the events (a.k.a actions) raised by your application into a single, predictable state.
While I was implementing the reducers and actions I thought: wouldn't it be really easy to centralize the game state and broadcast to multiple "players"? I then moved all of the game processing logic: the game update interval, rule evaluation and player events into a Node.js server, hooked up some web socket actions and thus "multilife" was created.
Although there are existing frameworks for using WebSockets with Redux, e.g. redux-websocket, I decided to write my own, as there are only 4 simple actions required:
-
WS_OPEN
- connection open -
WS_SEND
- send a message to the server -
WS_RECEIVE
- receive a message from the server -
WS_CLOSE
- connection closed
I also needed more control over the format I sent and received messages in, using binary instead of JSON, as I describe in the Optimization section.
Colors
Now this is interesting, I thought! I could now broadcast the game state to multiple browsers, in real time! But... How could I make it more interesting? I decided to give each cell a color because it looks pretty! Every player is assigned a random color when they connect. The cells also mix colors when they reproduce, creating some interesting patterns.
Optimization
I found that serializing the entire game state and events in plaintext JSON was computationally very expensive and used a lot of bandwidth. I was talking to a colleague and they suggested to create a binary protocol, so I did! I also considered Protocol Buffers, but I preferred to serialize the data myself.
I knew the binary protocol would be especially tricky to implement, so I used a Test-driven-development approach: I wrote initially failing serialize
and deserialize
tests for the binary protocol, each asserting that it could successfully serialize and deserialize a protocol model and then wrote the code for each method until they all passed. Unit tests are invaluable when working with complex logic.
I used the color-namer module to name each color in the game state. However, it was inefficient at first - every time it looked up a color it iterates through the entire list of color names to compare color distance, an O(n)
(or linear time) operation and it did not cache the results of each color lookup. To improve the performance, I forked the repository and implemented Memoization by caching the results in a WeakMap. I used a WeakMap so that the Garbage Collector would intermittently clear the cache, instead of filling up the cache forever (there are 2^24 - 1
, or 16,777,215 possible colors that could be looked up ... ). I also implemented support for the Delta-E color distance function for more accurate naming of colors. I submitted both these changes as a pull request to the module maintainer and they were eventually accepted and released.
Add Memoization, deltaE distance support #9
Deployment and Release
It was now time to show the world what I had created! But how? Well, to host a website, I needed a server. I created a Droplet on DigitalOcean to host multilife. I also purchased a domain: multilife.live. (edit: I did not renew the domain, and it has since expired and is parked by someone else now!)
I set up Nginx to host the site, and pm2 to run the app, as well as LetsEncrypt to provide SSL.
I also set up CI/CD using CircleCI so that I did not have to manually deploy to production whenever I merged new code into master. CircleCI also runs my tests before deploying.
After many attempts to get CI/CD working (many, many "fixing CI" commits), multilife was released and I shared it with my friends. We had a lot of fun clicking around and watching the patterns form. The site also uses responsive design, so everyone had their phones out touching on their screens!
MultiLife RGB
johnmerchant / multilife-rgb
Renders multilife.live to RGB LED Matrix hardware connected to a Raspberry Pi
MultiLife RGB
Renders the https://multilife.live game state to a LED RGB matrix panel connected to a Raspberry Pi
Dependencies
Building
# clone repos
cd ~
git clone https://github.com/jmercha/multilife-rgb
git clone https://github.com/hzeller/rpi-rgb-led-matrix
# build librgbmatrix
cd ~/rpi-rgb-led-matrix/lib
make
sudo cp librgbmatrix.so.1 /usr/lib
# build multilife-rgb
cd ~/multilife-rgb
make
I then wanted to take things a step further: what if I could render the game state to a RGB LED panel? Wouldn't that be cool? So I shopped around and purchased a 32x32 RGB LED matrix panel and a Raspberry Pi Zero W
When I was wiring up the RGB LED panel, I accidentally connected a wrong pin and broke all of the electronic components in the panel - I wired 5 volts into the ground pin. Oops! I had to wait another week for a new one to arrive before I could get started.
I learned a valuable lesson here: broken software is easy to fix, but you cannot easily fix broken transistors and capacitors.
I wrote a program in C - multilife-rgb to listen to game events from the server over UDP using my binary protocol and render the cells using the rpi-rgb-led-matrix library. I chose UDP over TCP as it made more sense for the case I was using it for - TCP is a stream-based protocol and UDP is datagram, or message-based. I also didn't mind if messages occasionally weren't received or were in the incorrect order.
I found it was very convenient to read the multilife binary protocol in C, I simply assigned a pointer to the protocol model structs to the received message buffers.
message.data = buffer + 1;
Although it did require using the packed attribute on the Cell
struct to align the data correctly.
The end result is visually appealing, especially in the dark. I love being able to tap cells in from my phone or desktop browser and seeing it instantly appear on the matrix.
Conclusion
Perhaps in the future, if "mutlilife" somehow goes viral (I doubt it), I could scale it out by using Redis and the Hashlife algorithm, as well as supporting zooming and panning in the frontend and protocol.
Learning by building things is fun, even if you are building something practically useless. I hope this might inspire others to learn by building the superfluous!
Top comments (0)