41.2 Solution

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).



CC BY-NC-SA 4.0 Bartlomiej Lukaszuk