This is the rules of Conway’s Game of Life:
- Any live cell with fewer than two live neighbors dies, as if by under-population.
- Any live cell with two or three live neighbors lives on to the next generation.
- Any live cell with more than three live neighbors dies, as if by overpopulation.
- Any dead cell with exactly three live neighbors becomes a live cell, as if by reproduction.
I represent Game of Life patterns in a set of text files. A dead cell is represented by a dot, a living cell by an o.
........... ...oo.oo... ...oo.oo... ....o.o.... ..o.o.o.o.. ..o.o.o.o.. ..oo...oo.. ........... ...........
Firstly, I format the pattern so that a node contains an integer Point, a tuple with the x/y position, as well as a boolean Status, stating if the node is alive or not. A Node is a tuple with a Point and Status. The list of all existing nodes constitutes the Game. Each game state is a Generation.
type Point = (Int, Int) type Status = Bool type Node = (Point, Status) type Game = [Node] type Generation = Integer isCharLiving ∷ Char → Bool isCharLiving char | char == 'o' = True | otherwise = False makeRow ∷ String → Int → [Node] makeRow row y = [((x,y), isCharLiving $ row !! x) | x ← [0..length row - 1]] prepareData ∷ [String] → Game prepareData rawData = concat [ makeRow (rawData !! y) y | y ← [0..length rawData - 1]]
nextState I map the list of Nodes, transforming them according to the rules of Game of Life.
nextState ∷ Game → Game nextState game = map (`makeNode` game) game
When quoting a few Haskell functions, the expressiveness of the type signatures become clear. We pass a Node, and then a Game (the currying is abstracted away in Haskell), and get a new Node.
We don’t care about the Point (it never changes), only the Status. We call
nextNodeState with two (implicitly curried parameters), a function call to
aliveNeighbours (counting the number of living neighbors), and the Status (alive or not).
makeNode ∷ Node → Game → Node makeNode node game = ( fst node, nextNodeState (aliveNeighbours game node directions 0) (snd node) )
Due to Haskell guards, evaluating predicates, we can formulate the rules very clearly in
nextNodeState ∷ Integer → Bool → Bool nextNodeState aliveNeighbours status | aliveNeighbours == 3 && not status = True | aliveNeighbours == 2 && status = True | aliveNeighbours == 3 && status = True | otherwise = False
and begin counting neighbors,
aliveNeighbours ∷ Game → Node → [Point] → Integer → Integer aliveNeighbours game ((x,y), status) dirs count | null dirs = count | isAlive game (x + fst (head dirs), y + snd (head dirs)) = aliveNeighbours game ((x,y), status) (tail dirs) (count + 1) | otherwise = aliveNeighbours game ((x,y), status) (tail dirs) count directions ∷ [Point] directions = [(0,-1), (1,-1), (1,0), (1,1), (0,1), (-1,1), (-1,0), (-1,-1)] isAlive ∷ Game → Point → Bool isAlive game node | isNothing(getCell node game) = False | snd (fromJust(getCell node game)) = True | otherwise = False getCell ∷ Point → Game → Maybe Node getCell pos  = Nothing getCell pos (((x,y), status) : rest) | pos == (x,y) = Just ((x,y), status) | otherwise = getCell pos rest
This pattern is subsumed the rules and the game continues until the application is ended.
main ∷ IO () main = do rawData ← readFile "./pentadecathlon" get (prepareData $ lines rawData) representation ∷ Status → String representation cell | cell = "[●]" | otherwise = "[∙]" putCell ∷ Cell → IO () putCell cell | fst (fst cell) == 0 = putStr $ "\n" ++ representation (snd cell) | otherwise = putStr $ representation (snd cell) clearScreen ∷ IO () clearScreen = putStr "\ESC[2J" get ∷ Game → IO () get game = do sequence_ [putCell cell | cell ← game] clearScreen threadDelay 200000 get (nextState game)