Let’s start with our universe.
const N_COLS = 80
const N_ROWS = 40
const PROB_ALIVE = 0.25
const Universe = Matrix{Bool}
function getEmptyUniverse()::Universe
return zeros(Bool, N_ROWS, N_COLS)
end
function getRandUniverse()::Universe
# rand gives val [0-1) and not [0-1] like prob, but it will do
return rand(Float64, N_ROWS, N_COLS) .<= PROB_ALIVE
end
For that we defined a few constants and functions. The Universe data type, is a synonym for a Matrix (N_ROWSxN_COLS) of Bools. This is a natural choice since each cell can be either alive (with the probability of 0.25) or dead.
Next, time for printing.
const ALIVE_SYMBOL = 'O'
const DEAD_SYMBOL = '.'
const N_GENERATIONS = 50
function getFieldSymbol(field::Bool)::Char
return field ? ALIVE_SYMBOL : DEAD_SYMBOL
end
function printUniverse(universe::Universe, nGeneration::Int)::Nothing
population::Int = sum(universe)
print("Generation: $nGeneration/$N_GENERATIONS, ")
println("population: $population\n")
for r in 1:N_ROWS
println(map(getFieldSymbol, universe[r, :]) |> join)
end
return nothing
end
# https://en.wikipedia.org/wiki/ANSI_escape_code
function clearDisplay(nLinesUp::Int)::Nothing
@assert 0 < nLinesUp "nLinesUp must be a positive integer"
# "\033[xxxA" - xxx moves cursor up xxx LINES
print("\033[" * string(nLinesUp) * "A")
# "\033[0J" - clears from cursor position till the end of the screen
print("\033[0J")
return nothing
end
function reprintUniverse(universe::Universe, nGeneration::Int)::Nothing
clearDisplay(N_ROWS+2) # +2 cause info line and newline
printUniverse(universe, nGeneration)
return nothing
end
The above (printing and reprinting) is basically a modified code from Section 34.2. Of note, here we do not draw the borders, instead we use dead (DEAD_SYMBOL) and live (ALIVE_SYMBOL) cells.
Time to determine the next state of our universe. But first we need to know the number of a cell’s neighbors that are alive.
function isCellWithinRange(row::Int, col::Int)::Bool
return (1 <= row <= N_ROWS) && (1 <= col <= N_COLS)
end
function getNumLiveNeighbors(universe::Universe, row::Int, col::Int)::Int
if !isCellWithinRange(row, col)
return 0
end
nAlive::Int = 0
neighborCol::Int, neighborRow::Int = 0, 0
for c in -1:1, r in -1:1
neighborRow, neighborCol = row+r, col+c
if !isCellWithinRange(neighborRow, neighborCol)
continue
end
if (neighborRow == row && neighborCol == col)
continue
end
if universe[neighborRow, neighborCol]
nAlive += 1
end
end
return nAlive
end
We assign this task to getNumLiveNeighbors that accepts our universe and the cell of interest coordinates (row and col) as its parameters. The neighbors of a cell are located in a row below, the same row as the cell, and a row above the cell’s own row (r in -1:1). Similarly, we look at a column to the left, the same column as the cell, and a column to the right of the cell’s own column (c in -1:1). Hence, a neighbor’s coordinates are calculated as neighborRow = row+r (row is the cell’s own row, r is the row shift) and neighborCol = col+c (col is the cell’s own column, c is the column shift). We examine all the possible neighbor locations with the for loop. If the coordinates fall outside the grid (!isCellWithinRange - the cell does not exist in our universe) then we continue to the next iteration (we examine next coordinates). The same goes for examining the coordinates of the cell itself (since both r and c may be equal 0). Otherwise, if a neighbor is alive (if universe[neighborRow, neighborCol]) we add 1 to the count (nAlive += 1), which we eventually return from the function.
Now we are ready to calculate the next state of our universe.
function shouldCellBeAlive(universe::Universe, row::Int, col::Int)::Bool
nLiveNeighbors::Int = getNumLiveNeighbors(universe, row, col)
if universe[row, col] && nLiveNeighbors in 2:3
return true
end
return nLiveNeighbors == 3
end
function getUniverseNextState(universe::Universe)::Universe
newUniverse::Universe = getEmptyUniverse()
for c in 1:N_COLS, r in 1:N_ROWS
newUniverse[r, c] = shouldCellBeAlive(universe, r, c)
end
return newUniverse
end
We start by figuring out if a cell should be alive in the next turn (shouldCellBeAlive). Per task specification, if a cell was previously alive (if universe[row, col]) and it got 2 or 3 live neighbors (nLiveNeighbors in 2:3) then it should be alive (return true). Otherwise, it should live if it was previously dead and got exactly three live neighbors (nLiveNeighbors == 3).
All that’s left to do is to getUniverseNextState by examining each cell (r and c) in the universe and deciding its fate in the next turn (shouldCellBeAlive(universe, r, c)).
And now for the final touch.
const DELAY_SEC = 0.5
# early stop
function areAllCellsDead(universe::Universe)::Bool
return sum(universe) == 0
end
function runGameOfLife()
universe::Universe = getRandUniverse()
printUniverse(universe, 0)
for nGeneration in 1:N_GENERATIONS
universe = getUniverseNextState(universe)
reprintUniverse(universe, nGeneration)
sleep(DELAY_SEC)
if areAllCellsDead(universe)
println("All cells are dead.")
break
end
end
end
Let the games begin (type runGameOfLife() and see what happens).