-- Back to the tutorials index--

Tutorial 9: Revisiting Conway's Game of Life

This tutorial illustrates how GeoMaestro can be used on the programming side, as a set of high-level KeyKit functions, the GUI being a simple window open on a hard-coded process. This is definitely not the tutorial you should start with.

The idea here is to provide utilities for mapping into music the dynamism of the cells from John Conway's Game of Life evolving patterns. It will be quite straightforward to adapt this approach to other cellular automatas.

Instant gratification:
What follows is quite verbose. If you want to have an insight of what it is all about, do this: open a GeoMaestro GUI on an empty scene, click on [proj] once to have projections display on, then click on [pj], select "PROJECTORS" on the projectors menubutton, click on [--->] and enter

(the code for this example function is described below)

First I will describe a set of low-level functions implementing the Game of Life and discuss how we can map this into music, then I'll introduce a couple of examples using these functions, and eventually we will see in detail a powerful high-level function allowing personal research on the many different ways to get music out of the automata.

If you do not already have a Game of Life simulator, here are two very good (and free) softs for Windows:

MCell (download at
Life32 (download at

Implementing the Game Of Life

All code discussed here lives in the file userlib/tut9_life.k from the GeoMaestro distribution. You should open it right now in a text editor, since I'm going to comment it...

The implementation is performed by the first set of functions:
InitLife(sizex, sizey, ch, mode, rules)
ImportLife105(fname, pflag)
SetLifeMap(mname, ...)
The InitLife() function creates the global variables we will need and give them some default values: GameSetX and GameSetY define the geometry of the game (the number and disposition of cells), GameChannel is the GeoMaestro channel number to be used for initial game events, GameSetMode defines the behaviour of the boundaries of the game set, GameRules is a string defining the rules (see below). These five parameters are set by the arguments of InitLife(), the three last ones being optional: by default the channel number will be 20, GameSetMode will be 1 ("wrap" mode: toroïdal topology) and the rules will be "23/3". These default values are only applied if the variables were not previously defined, though; otherwise they will keep their previous values.

Also created by InitLife() are the arrays GameSet and GameSetBuffer, used to perform the calculations.

Eventually, InitLife() also gives a default map LifeMapIs if none was already defined. We will see what is a map later on.

Note the RemVAR() call at the end of InitLife()

SetGameRules() takes a string as arguments, which is the set of rules in the classic survival/birth format (I will not attempt to explain what is the Game of Life here; see the documentation in the softs I recommended at the beginning of the tutorial for details). It sets the value of the GameRules string, which is created if it did not previously exists. Note the RemVAR() call again.

NextGeneration() performs the main calculations: it brings GameSet from a state to the next one, using GameSetBuffer in the meantime. It also increases its optional argument, which is a little extra feature.

GameSetBorders() is called by InitLife() and by NextGeneration(): depending on the value for GameSetMode, it either empty all cells around the game set, or it uses them to duplicate the borders of the set (up to down, down to up, right to left and left to right) so that the opposite boundaries are connected: a glider leaving the set by the right will immediately enter it by the left ("wrap" mode).

MapLifeToEv() is the function which arrange the events so that they fit the game set, by first killing all events born from a previous mapping (which are marked by a specific label: "GameOfLife"), then by calling the function LifeMap() for each living cell (see below for more), which creates a new set of "GameOfLife" events. Its argument is a rectangular area (in the xyd() format) defining the place for the game set in the event scene.

RemoveAllLifeEvents() deletes all events labelled "GameOfLife".

ImportLife105() reads a .lif file in the Life 1.05 format (a well-known ASCII standard making files easily editable by hand: see gliders.lif) and loads the pattern into GameSet. If the pattern is too broad, only its center part is loaded. The argument is a file name: if no argument is given, a browser window will be opened so that you can go and fetch your file. The function returns a value -1 if no file has been found.

NextSteps(n) is an higher-level function which calls n times NextGeneration(), then calls MapLifeToEv(), then redraw the GUI. It will only work properly if a GUI is open...

LifeMap() performs the mapping: it uses the function whose name is stored in LifeMapIs and call it with the arguments n (number of generation the cell has been alive if it is, 0 if it's dead), i and j (coordinates of the cell in GameSet), plus the parameters specific to the mapping function stored in LifeMapArguments. More on this below.

SetLifeMap() sets which mapping function we want to use, and which parameters we want to give it. Note the RemVAR() call at the end.

EvToGameSet() looks for all GameChannel events in a given xyd() area (its first argument), marks them as "GameOfLife" events (so that they will be killed at the next MapLifeToEv() call), and maps them into GameSet. This allows for a direct drawing of the initial pattern. It also keeps all existing "GameOfLife" events, whatever their channel, so that it can also be used to resume a stopped evolution

Mapping the game set to the Ev scene

So, how do we map our cells into events ?

Living cells are characterized by three integers: n is the number of generations the cell has been alive, i and j define its position in the game set. We will use these numbers as parameters for the mapping process.

MultiChannels() is a very basic example of mapping function. It uses the three arguments we just talked about (n, i, j) plus three other ones: two integers defining a range of channels, and a phrase. The nodur of an event will be this phrase, affected to a channel corresponding to the value of n: at the first generation, an event is on channel 1; at the second one, it is on channel 2, etc... until we reach the last channel on the range, in which case it doesn't change any more, or until the cell associated to the event dies, in which case we will start from scratch the next time it's born again. The function returns the nodur and the channel value in two different fields of an array.

This example function does not make use of the i and j parameters, but they have to be there, it's required by the way we command GeoMaestro to use this function:
SetLifeMap("MultiChannels", 1, 10, 'a')
... will set the values of LifeMapIs and LifeMapArguments so that we will use MultiChannel() with parameters (n, i, j, 1, 10, 'a').

This way, a single command as the one above allows us to choose among a collection of mapping functions the one we want to use, and at the same time to give it the parameters we want; the overall thing is thus very flexible (actually, the management of distortion functions by the main GeoMaestro GUI does not work differently).


The "tutorial examples" section in userlib/tut9_life.k provides a few functions illustrating GeoMaestro programming. So far, we have simply been implementing the low-level functions necessary to make interesting work fun and painless. So let's go !

Let's start by having a look at the LifeOnC1() function:
Note that no mapping function has been defined at all, which means we will use the default one defined in InitLife(), that is MultiChannel() with arguments (1,5,'av127')

Ok, let's try it! Open the GeoMaestro GUI, set up some interesting distortion functions for channel 1 to 5, say: ... we will keep a piano for all channels (Ev[ch]["PAC"] is '' for all ch, as defaulted).

If you feel lazy, instead of setting the distortion functions you can directly move this file: tut9.df in your DATA directory (for example) and load it from the GUI with the [R] button. It defines all the previous settings.

now click [proj] once, then select the "PROJECTORS" item in the projectors menu, click [--->] and type this at the pop-up window prompt:
... the browser window should open. Go to your DATA directory and select gliders.lif

What you should see next are three gliders slowly evolving in the graphic area, along with their projections on C1.

When the motion stops, after 50 generations, click on [hear] to hear the result, or on [snarf] to snarf it.

This is what you should get: gliders50.mid

The second example is LifeOnAB(), which is very similar to LifeOnC1() but uses another support for projection, and allows it to be changed at each generation, according to the following arguments:
LifeOnAB(a, b, ngen, thoffset)
... here a and b are two points defining a segment. If thoffset is given, it is the angle this segment will be rotated by at each generation (ngen is again the number of generation to calculate)

So this time first create two points AA and BB (ith the "new point" mouse mode), like this:

then click [--->], type:
... and select gliders.lif again.

You should see the three gliders moving as previously and being projected on a rotating segment, so that it ends like this:

(here is the phrase I got: gliders_AB.mid)


The function LifeOnSchedule() extends what we just saw to provide a customizable set-up. It takes a single argument, called a schedule in the following, which is an array containing parameters values and a program for the projections.

Here are the different fields (most of them being optional) in the schedule array:
"Ngen"	number of generations to calculate
"X"	(opt.) X-size of the game set (default GameSetX = 21)
"Y"	(opt.) Y-size of the game set (default GameSetY = 21)
"area"	(opt.) area for the game set in the scene (default xy(-1,-1,1,1))
"lif"	(opt.) full name for the *.lif file, or "" (see below)
"rules"	(opt.) game rules (default "23/3")
"ch"	(opt.) GameChannel (default 20)
"df"	(opt.) distortion settings (see below)
"snarf" 	(opt.) snarf mode (default 0, see below for more)
"zoomcircle"	(opt.) zoom range (a circle)
... then come integer indexes, from 1 to schedule["Ngen"], each one corresponding to a given generation. If an index is not present, the corresponding generation will not be mapped into events. Each integer indexes an array with the following fields:
"proj"	projector call for that generation	
"map"		(opt.) LifeMap to use
"mapargs"	(opt.) LifeMap arguments
"pause"		(opt.) see below

Of course the schedule is supposed to be generated by a program, since it is quite an heavy data structure. Let's see with an example:

function LifeExample1()
    sched = ["Ngen" = 100,
	     "lif" = DATA+"gliders.lif",
	     "zoomcircle" = Cerc(xy(0,0),1.2),
	     "df" = "tut9"

    for (n= 1; n<= 100; n++)
	if (n/2 == n/2.0)
	    sched[n] = ["proj"= "Ecoute(Oi,Oj)"]
	    sched[n] = ["proj"= "EcouteC(Cerc(Or,"+string(0.2+0.1*sin(n))+"),Oi,1)"]

    return (LifeOnSchedule(sched))

The only thing this function does is define a shedule and call LifeOnSchedule() with it. You can see that the schedule contains a "lif" field; the projectors only rely on the points Or, Oi and Oj which are defined when GeoMaestro starts. As a consequence, no extra set-up is needed. You can simply open the GUI, click on [--->] and enter
... then watch the display. Note that while the thing happens, you are free to change the zooming or the projection display by clicking the corresponding buttons.

The "df" field, if present, is the name of a distortion function file in the DATA directory. In this example the distortion function settings will be read in the file DATA+"tut9.df". Such a file is created by clicking the [S] button in the GUI.

The second example is a variation on the first one:

function LifeExample2()
    sched = ["Ngen" = 100,
	     "lif" = ""

    for (n= 1; n<= 100; n++)
	if (n/2 == n/2.0)
	    sched[n] = ["proj"= "Ecoute(Oi,Oj)", "pause"= 1]
	    sched[n] = ["proj"= "EcouteC(Cerc(Or,"+string(0.2+0.1*sin(n))+"),Oi,1)"]

    return (LifeOnSchedule(sched))

This time the "lif" field is an empty string: this means that the seed pattern is the Game events in the current scene. Game events are either events with a "GameOfLife" label, such as the ones left other by a previous calculation, or any events in the GameChannel, which is the default one in our example, that is 20: so you can draw the initial pattern by simply creating events in channel 20 (their nodurs doesn't matter; their positions can be approximative.)

Another difference is the "pause" field that appears in the schedule every other generation: this means, as you can see if you try it, that at such generations the calculation stops and waits for keyboard input: press any key to resume. If you press "s", the process stops here. If you press TAB, it will not stop any more until the end (after "NGen" generations).

The value 1 for "pause" is not meaningless: it sets the Snarf so that it contains all music projected since the last pause occurred. Any other value for "pause" sets Snarf so that it contains all music projected from the beginning. This is an interesting difference as you can permanently see the Snarf value in the small phrase window at the right of the GUI, between the two groups of buttons (by the way, if this window is an annoyance to you, you can desactivate it by setting SnarfWindowIsOn to 0)

Another Snarf setting is the "snarf" field in the schedule: if 0, each generation is Snarfed; if 1, each generation is added to Snarf.

(more here later ...)

-- Back to the tutorials index--
-- Back --