Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / game

Conway's Game of Life with F# and SDL.NET

5.00/5 (1 vote)
4 Feb 2011CPOL5 min read 22.1K   227  
This is an implementation of Conway's Game of Life using F# and SDL.NET
gameoflife.png

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.

F#
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.

F#
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.

F#
 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.

F#
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.

F#
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.

F#
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.

F#
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 strings or throws an exception if the file is not found. It then loads the data using one of two functions depending on the format.

F#
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.

F#
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.

F#
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.

F#
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.

F#
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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)