This is a Haskell implementation of Conway's Game of Life.
It plays on a console. It should be able to play a field of any size, but we only give it a glider on a small field to run at this point.
Using a file named GameOfLife.hs
:
import Control.Concurrent
import Text.Printf
main :: IO ()
main = gameOfLife (
[ [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
, [0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0]
, [1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0]
, [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
, [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
, [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
, [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
, [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
, [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
, [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
] )
The main
function only calls gameOfLife
with some startup state - a list of lists of 0s and 1s:
gameOfLife :: [[Integer]] -> IO ()
gameOfLife state = do
pretty_print state
let new_state = transition state
sleep
printf "\ESC[%dA" $ length state -- move cursor to beginning.
gameOfLife new_state
First we pretty print the state, define the new state as the transition of the original state, sleep (to slow it down enough to visualize the changes), move the cursor to the beginning using an escape code so we can draw the new state over the old, and then recursively call gameOfLife with the new state.
The pretty print function is as follows - it uses a sprite function to map the 0's and 1's to a viewable field of characters:
pretty_print :: [[Integer]] -> IO ()
pretty_print state = do
mapM_ print_row state
where
print_row linestate = do
putStrLn $ concat $ map sprite linestate
sprite :: Integer -> String
sprite 0 = "."
sprite _ = "*"
"Sprite," is probably the wrong word. Maybe, "char," would be better?
When printed - it looks like this - here's sample output:
$ ./GameOfLife
.*..........
..*.........
***.........
............
............
............
............
............
............
............
Then the new state is defined as the transition of the old state.
We start with the row of interest, and get the rows before it and after it. Then we go element by element. When we would get an index that would be out of bounds, I get the index from the other end of the field. This adds some time to the algorithm, I'm sure, but it has the nice effect of allowing the field to wrap around.
-- get new state row by row.
transition :: [[Integer]] -> [[Integer]]
transition state = do
process 0
where
last_i = ((length state) - 1)
process n = process_row n : (if n == last_i then [] else process (n + 1))
process_row i = process_rows prev_row this_row next_row
where
prev_row = state !! (if i == 0 then last_i else (i - 1))
this_row = state !! i
next_row = state !! (if i == last_i then 0 else (i + 1))
process_rows prev row next = do
proc 0
where
last_j = ((length row) - 1)
proc m = proc_col m : (if m == last_j then [] else proc (m + 1))
proc_col j = live_die (row !! j) (
-- column to left
(if j == 0 then (last prev + last row + last next) else
(prev !! (j - 1) + row !! (j - 1) + next !! (j - 1)))
-- above & below
+ prev !! j + next !! j
-- column to right
+ (if j == last_j then (prev !! 0 + row !! 0 + next !! 0) else
(prev !! (j + 1) + row !! (j + 1) + next !! (j + 1)))
)
The logic for does the cell live or die is if the cell is currently alive, then it stays alive if 2 or 3 surrounding cells are alive, else it dies. If the current cell isn't alive, it only comes to life if there are 3 cells alive next to it. I believe I succinctly cover this logic with pattern matching:
live_die :: Integer -> Integer -> Integer
live_die _ 3 = 1
live_die 1 2 = 1
live_die _ _ = 0
Finally, each transition is followed by some sleeping to avoid having a crazy-looking blur on the screen - I have no problems with the animation with a sleep of 1/10th of a second per transition:
sleep :: IO ()
sleep = threadDelay 100000
I have stack installed, so I built it with:
$ stack ghc GameOfLife.hs
(A friend had to build with ghc -dynamic GameOfLife.hs
because he just installed GHC with Arch or Manjaro's package manager, pacman
.)
Make the executable file actually executable (only had to do this the first time.)
$ chmod -x GameOfLife
and execute it like so:
$ ./GameOfLife
.*..........
..*.........
***.........
............
............
............
............
............
............
............
Ideally, the executable could take a value to seed a pseudo-random number generator (PRNG) or a filename with a user-created image. For those goals, I suppose it's obvious that main
shouldn't unconditionally start gameOfLife
like it does.
Probably the printing should be further separated from the transition
.
Maybe we could use data types to represent the cells (0's and 1's) and the field (a list of lists of Integers). But we would need (+)
implemented for Cell values.
I think I can see a few other ways to slightly reduce redundant function calls, but I'm not sure I'll save any lines of code that way.
array
package instead of lists, which is wired in with ghc, but it is still an external dependency, if you think about. \$\endgroup\$