Introduction
Overview
This is an implementation of Conway's Game of Life using F# and SDL.NET. The Game of Life is a zero-player game invented by British mathematician John Conway in 1970. The rules of the Game of Life are simple, but complex behaviour can emerge as a result of these rules.
About SDL.NET
To build the project, SDL.NET is required. SDL.NET provides .NET bindings written in C# for the SDL gaming library. To create this game, I installed the sdldotnet-6.1.1beta-sdk-setup.exe version from http://cs-sdl.sourceforge.net/.
How to Run the Game of Life
Rules
By default, this program uses the rules from Conway's Game of Life: an alive cell with two or three alive neighbours stays alive in the next generation, and a dead cell with exactly three live neighbours becomes alive. Other rules are possible, and can be set by loading a .lif file into the program from the command line.
Mouse and Keyboard Commands
Mouse commands:
- Left-click - makes a cell alive
- Right-click - makes a cell dead
Keyboard commands:
- Enter - starts the game
- Pause - pauses the game
- Backspace - clears the grid
Loading Data from a .lif File
This module reads Game of Life files in two formats: Life 1.05 and 1.06. Example files are included with the program in the Data folder. To load data into a Game of Life grid, run the program with the path to the file as a command line argument, e.g.: GOL.exe C:\Life\Data\highlife.lif.
Game of Life Implementation
Project Structure
The GOL project contains 4 modules:
- GOL - The main entry point for the program, this handles graphics display, keyboard and mouse input and command line arguments
- GOLGrid - Holds the state of the grid of alive and dead cells and handles their changes in each generation
- ReadLife - Loads data from .lif files into the game grid
- General - A collection of general purpose functions and active patterns used in the project
GOL
The go
function is called when the application is run. It gets command-line arguments, if any, and adds handlers for mouse and keyboard events.
let go() =
let args = System.Environment.GetCommandLineArgs()
if args.Length > 1 then readFromLifeFile args.[1]
clearScreen()
Events.KeyboardDown.Add(HandleInputDown)
Events.MouseButtonDown.Add(HandleMouseClick)
Events.Quit.Add(quit)
Events.Tick.Add(update)
Events.Run()
go()
The function HandleInputDown
uses pattern matching to select the function to call when a particular key is pressed.
let HandleInputDown(args : KeyboardEventArgs) =
match args.Key with
| Key.Escape ->Events.QuitApplication()
| Key.Backspace -> clearGrid()
| Key.Return -> runGame()
| Key.Pause -> pauseGame()
| _ -> None |>ignore
HandleMouseClick
sets a cell to alive with a click from the primary button, which is usually the left button, and to dead with any other mouse button, if the game is paused or has not yet started.
let HandleMouseClick(args : MouseButtonEventArgs) =
if isRunning = false then
let x = args.X
let y = args.Y
match args.Button with
| MouseButton.PrimaryButton -> setCell (int y) (int x) true
drawCell x y cellColour
| _ -> setCell (int y) (int x) false
drawCell x y Color.White
drawGrid()
GOLGrid
A cell in the game of life can be either alive or dead.
type private State =
| DEAD = 0
| ALIVE = 1
By default, a cell is born when it has 3 live neighbours, and stays alive with 2 or 3 live neighbours. These rules can be changed by using the ReadLife
module to load a .lif file.
let mutable private born = [3]
let mutable private survive = [2;3]
let setRules birthList survivalList =
born <- birthList
survive <- survivalList
The newValue
returns an ALIVE
or DEAD
value for a cell on the grid depending on the total number of live neighbours it has. It uses an active pattern IsMember
to check if the total is in the born
or survive
lists.
let private newValue row col =
let total = countNeighbours row col in
match total with
| IsMember born when isDead row col -> State.ALIVE
| IsMember survive when isAlive row col -> State.ALIVE
| _ -> State.DEAD
The next
function sets the values of the cells in the next generation nextGrid
based on the values of the current grid
, then updates grid
.
let next() =
for row = 0 to (height-1) do
for col = 0 to (width - 1) do
nextGrid.[row,col] <- newValue row col
for row = 0 to (height-1) do
for col = 0 to (width - 1) do
grid.[row,col] <- nextGrid.[row,col]
ReadLife
The ReadLife
module can load data into the GOL program from two formats - Life 1.05 and 1.06. More information about these formats can be found at Cellular Automata files formats.
The readFromLifeFile
function reads a file into an array of string
s or throws an exception if the file is not found. It then loads the data using one of two functions depending on the format.
let readFromLifeFile lifeFile =
try
let lifeData = File.ReadAllLines(lifeFile)
match lifeData.[0] with
| "#Life 1.05" -> createLife1_05 lifeData |> ignore
| "#Life 1.06" -> createLife1_06 lifeData.[1..]
| _ -> None |> ignore
with
| :? System.IO.FileNotFoundException
-> printfn "System.IO.FileNotFoundException:
The file %s was not found" lifeFile
| _ -> printfn
"Exception occurred when trying to read %s" lifeFile
As it reads each line of the file, createLife1_05
sets the survival and birth rules for the game for a line starting with #N
or #R
, sets the offset from the centre for a line starting with #P
, or passes the line to the inner function setCellsLife
to set the cells of the grid to dead or alive. Other lines such as description lines starting with #D
or blank lines are ignored. The variables X
and Y
are not used outside of createLife1_05
and setCellsLife
and so these are closures using mutable reference cells.
let private createLife1_05 (lifeData : string[]) =
let X = ref 0
let Y = ref 0
let setCellsLife (line : string) =
let startX = !X
for ch in line do
match ch with
| '.' -> setDead !Y !X
| '*' -> setAlive !Y !X
| _ -> None |> ignore
X := !X + 1
Y := !Y + 1
X := startX
for line in lifeData do
match line with
| StartsWith "#N"-> setRules [3] [2;3]
| StartsWith "#R"-> let [| _ ; survival ; birth |]
= line.Trim().Split([|' ';'/' |])
setRules (stringToIntList birth)(stringToIntList survival)
| StartsWith "#P"-> let [|_; x ; y|] = line.Trim().Split(' ')
X := Int32.Parse( x) + centreCol
Y := Int32.Parse (y) + centreRow
| StartsWith "."
| StartsWith "*" -> setCellsLife line
| _ -> None |> ignore
General
The General module holds general purpose functions and active patterns. Active patterns are for pattern matching on the results of function calls. StartsWith
checks if a string
starts with a substring and is used in createLife1_05
in the ReadLife
module.
let (|StartsWith|_|) substr (str : string) =
if str.StartsWith substr then Some() else None
IsMember
checks if an item is in a list and is used in the newValue
function of the GOLGrid
module.
let (|IsMember|_|) list item =
if (List.tryFind (fun x -> x = item) list)
= Some(item) then Some() else None
The function stringToIntList
uses pipelines |>
to return a list of integers when given a string
of digits. This is used in createLife1_05
to get lists of integers to set the birth and survival rules.
let stringToIntList (str : string) =
str.ToCharArray() |> Array.map (fun x -> x.ToString())
|> Array.map (fun x -> Int32.Parse(x))
|> Array.toList
Examples
These example patterns can be found in the Data folder.
Glider - glider.lif
The glider is a diagonally moving pattern that takes four generations to resume its original shape. The rules of the Game of Life say nothing about movement, but moving patterns appear as a consequence of those rules.
Oscillators - oscillators.lif
Oscillators are cyclic patterns that stay in one place. The patterns from left to right in oscillators.lif are the blinker, pulsar and pentadecathlon.
R-pentomino - r-pentomino.lif
This begins as a simple five cell pattern but generates many patterns before finally settling into a stable state. The glider was first discovered in the R-pentomino.
Glider gun - 13glidercollision.lif
The glider gun generates an endless stream of gliders, and is created by a collision of 13 gliders.
Highlife replicator - highlife.lif
HighLife has slightly different rules to Conway's Game of Life: a cell survives if it has 2 or 3 neighbours and is born if it has 3 or 6. The replicator pattern in HighLife
reproduces copies of itself.
History
- 4th February, 2011: Initial post