Game of life ∈ artus9033 (original) (raw)

The game presents a PoC that was fun to make, utilizing state-of-the-art technologies available. The renderer used is WebGL, with

three.js's

abstraction on top of it. As the website is written in React, the game scene is built up declaratively in JSX using

@react-three/fiber

and finally, it utilizes

Tone.js

to dynamically synthesize polyphonic sound based on the quantity of alive cells in each sector of the map. All components displaying equations on this page use react-latex & katex for rendering LaTeX code.

The logic of the game is classical

Conway's Game of Life

rules. To optimize performance, the logic was implemented in Rust that is compiled to WASM with

wasm-bindgen

. The renderer, as described, is a React component utilizing

three.js

using the

@react-three/fiber

React abstraction. Moreover, all cells are just

THREE.instancedMeshes so as to be performant enough. For a slightly more artsy experience, I also created a ‘vanishing’ effect for cells that were alive the round before, but are dead now - for just one render, they are semi-transparent (or, more precisely, ‘more transparent’ than cells being currently alive), making a pleasant animation effect.

The arena (map) is generated dynamically by dividing it into a grid and placing items picked from a set of pre-defined structures (e.g.

blinker

,

pulsar

,

beacon

,

tumbler

,

queen bee

) that have a pre-set probability weight index using

rand::distributions::WeightedIndex

that differentiates the probability of picking a specific structure. The templates for these structures are defined as 2D matrices and cached to be generated just once with the

once_cell::sync::Lazy

wrapper.

This implementation features a controlled frameloop that executes both the game logic and rendering sequentially at a given rate specified in the floating game settings. The game logic is implemented in

Map::morph_map_next_round

and uses a reference to the

THREE.InstancedMesh

that is bound with

Map::bind_js_cells_instanced_mesh

to a

RefCell<JsValue>

.

Before the game loop starts, the WASM module is initialized, bound to the JS objects (THREE.InstancedMesh

instance and cell colors for cell state lookup table object), the

THREE.InstancedMesh

is populated with a proper quantity of instances, for each of them a static transform is set to appropriately position them on the map, and

THREE.InstancedMesh.setColorAt(0, ...)

is called to initialize the

THREE.InstancedMesh.instanceColor.needsUpdate

property (as THREE.js requires it to be done).

Each game round, the Rust/WASM game logic:

A special feature of this implementation is polyphonic sound generated in a specific manner. The map is divided into cells of an arbitrary-sized grid (for the purpose here, I chose a

3×33 \times 3

grid). Each cell of such a grid contains part of the game map, therefore a checksum of its cells can be calculated - for simplicity, I decided to simply take the sum of cells that are alive.

That sum is afterwards turned into a cyclic index used for picking a note from the

circle of fifths

, utilizing it's even parity. For an index - the sum of alive cells in a ‘sound grid’ cell

SijS_{ij}

- with a cell of size

x×yx \times y - being:

∣∣Sij∣∣=∑p=i⋅x(i+1)⋅x∑q=j⋅y(j+1)⋅yMpq||S_{ij}|| = \sum_{p = i \cdot x}^{(i + 1) \cdot x}\sum_{q=j \cdot y}^{(j + 1) \cdot y} M_{pq}

where

MM is the matrix containing the map, such that Mpq={0if cell (p,q) is dead1if cell (p,q) is aliveM_{pq} = \begin{cases} 0 & \text{if cell }(p, q)\text{ is dead} \\ 1 & \text{if cell }(p, q)\text{ is alive} \end{cases}, is a cyclic key to index a tones array. The tones array is chosen based on the key's even parity: either it is the minor or major subset TT from the circle of fifths: T[(∣∣Sij∣∣mod ∣∣T∣∣)+∣∣T∣∣]mod ∣∣T∣∣T_{[(||S_{ij}|| \mod ||T||) + ||T||] \mod ||T||}. Such tones for all the sound grid cells are then played simultaneously on the polyphonic synthesizer provided by Tone.js, uniformly distributed over the time foreseen for a single audio loop (a value of

23\frac{2}{3}s), with an additional oscillator to modulate signal frequency and an

AM envelope

. Additionally, a slight

portamento

is applied afterwards so as to make the transition between synthesized sounds smoother. The computed notes are visualized in the table above, marked with

chromestetic

colors associated with them.

Consecutive tones are mixed one-after-another using a polyphonic mixer, and are synthesized using either of voice synthesizers:

FMSynth

or

DuoSynth

, which is configurable and can be picked in the floating settings menu.