# A simple version of Conway's Game of Life using ASCII art

I'd like a general review of my version of Conway's Game of Life. Right now, it's very simple, and doesn't lead to anything interesting yet, as the "seed" generation is randomly generated and small; so it tends to die off rather fast.

import Data.Char
import Data.List
import qualified Display as D
import qualified Data.Map.Strict as M
import System.Random

type Coord      = (Int,Int)
type Population = [Coord]
type Generations= (Coord,[Population])

rChar :: IO Char
rChar = randomRIO (33,124) >>= return . chr

--Generates a random population of n cells with the range of (xMax,yMax)
randomPop :: Int -> Coord -> IO Population
randomPop n (xMax, yMax) = do
xs <- mapM (\_-> randomRIO (0,xMax)) [1..n]
ys <- mapM (\_-> randomRIO (0,yMax)) [1..n]
let datas = zip xs ys
return $foldr (\(x,y) acc -> birthCoord (x,y) acc) [] datas --Add a cell to the alive population if it isn't already alive birthCoord :: Coord -> Population -> Population birthCoord c pop | isOccupied pop c = pop | otherwise = c : pop --Removes a cell from the alive population killCoord :: Coord -> Population -> Population killCoord = delete --Returns a list of cells to check surrounding a coord getCheckList :: Int -> Coord -> [Coord] getCheckList range c@(cX, cY) = [(x,y) | x <- [cX - range..cX + range], y <- [cY - range..cY + range], (x,y) /= c] --Checks if a cell is occupied isOccupied :: Population -> Coord -> Bool isOccupied = flip elem --Checks for occupied neighbors checkCoordList :: Population -> [Coord] -> [Coord] checkCoordList pop = filter (isOccupied pop) --Checks how many neighbors a cell has checkAround :: Population -> Coord -> Int checkAround pop = length . checkCoordList pop . getCheckList 1 --Defines the rules by which a cell is revived or killed applyRulesTo :: Coord -> Population -> Population applyRulesTo c pop | neighs < 2 || neighs > 4 = killCoord c pop | neighs == 3 = birthCoord c pop | otherwise = pop where neighs = checkAround pop c --"Looks over" a row, as part of scanRange scanXAt :: Int -> Int -> Population -> Population scanXAt xMax y pop = foldr (\x pAcc -> applyRulesTo (x,y) pAcc) pop [0..xMax] --"Looks over" the specified range, and applies the rules to any coords it finds scanRange :: Coord -> Population -> Population scanRange (xMax,yMax) pop = foldr (\y pAcc -> scanXAt xMax y pAcc) pop [0..yMax] --Outputs a block of coords (debugging) showGen :: Generations -> String showGen (_,g) = concatMap (\frame -> show frame ++ "\n\n") g --Recursively applies the rules to a population for n generations simGens :: Int -> Coord -> Population -> Generations simGens n maxs seed = (maxs,foldr (\_ p@(lastPop : accPop) -> scanRange maxs lastPop : p) [seed] [1..n]) --Translate a population to a printable frame popToFrame :: Coord -> Population -> D.Frame popToFrame maxs = let resizedFrame = D.setFrameDims maxs D.newFrame in foldr (\c ddAcc -> D.addObj c "#" ddAcc) resizedFrame --Translates a Generations object to a printable "Movie" gensToMovie :: Generations -> D.Movie gensToMovie (maxs,gens) = reverse$ foldr (\pop fAcc -> popToFrame maxs pop : fAcc) [] gens

main = do
let maxs = (10,10)
rPop <- randomPop 50 maxs
let gens = simGens 50 maxs rPop
m = gensToMovie gens
D.playMovie 200 m


To display the results, I made an ASCII "screen" that I'd like looked at aswell. The biggest problem with the screen is the Coord type. I made it a new type instead of just aliasing (Int,Int) so I could override the Ord operations to have it display differently. It works, but it's uglier then a tuple IMO. I also don't have a way of producing a Movie inside of the screen; it needs to be created in whatever is using the screen.

import qualified Data.Map as M

data Coord      = Coord Int Int deriving (Eq)
type CoordTup   = (Int, Int)

instance Ord Coord where
(Coord x1 y1) <= (Coord x2 y2) =
y1 < y2 || (y1 == y2 && x1 <= x2)

instance Show Coord where
show (Coord x y) = "(" ++ show x ++ " ," ++ show y ++ ")"

data FrameSettings = FrameSettings {frameMaxs :: CoordTup,
frameSpacing :: Int}

type Frame = (FrameSettings,M.Map Coord String)
type Movie = [Frame]

defaultSettings = FrameSettings (0,0) "" 3

newFrame = (defaultSettings, M.empty)

--Sets the max dimensions to display in a frame
setFrameDims :: CoordTup -> Frame -> Frame
setFrameDims maxs (fs,d) =
(fs { frameMaxs = maxs },d)

--Pads a list of strings so everything lines up, then concats them
pad :: Int -> [String] -> String

addMSDelay :: Int -> IO ()
| ms < bound = threadDelay $1000 * ms | otherwise = error$ "addMSDelay can\'t exceed " ++ show bound
where
bound = (maxBound :: Int) div 1000

--Turns a frame into a table to be displayed
frameToTable :: Frame -> [[String]]
frameToTable ((FrameSettings (xMax,yMax) nFill spacing),d) =
map (\y -> map (\x -> getDisp x y) [0..xMax]) [0..yMax]
where
getDisp x y = case M.lookup (Coord x y) d of
Just d  -> d
Nothing -> nFill

--Concats a table into a string with newlines
showTable :: Int -> [[String]] -> String
showTable spacing = concatMap (\c -> (pad spacing c) ++ "\n")

--Prints a Frame
printFrame :: Frame -> IO ()
printFrame f@((FrameSettings _ _ s),_) =
putStrLn . showTable s $frameToTable f --Adds to the list of coords to show addObj :: CoordTup -> String -> Frame -> Frame addObj (x,y) dispChar (ds,dd) = (ds,M.insert (Coord x y) dispChar dd) --"Plays a movie", inserting a delay in between frames playMovie :: Int -> Movie -> IO () playMovie delay = mapM_$ \frame -> do
printFrame frame


Any and all feedback would be appreciated. I'm planning on adding to it to make it easier to use, but before I do that, I'd like to know if it's a good design to build on, or if it needs to be redone.

The main file includes the display as "Display", so to try it, they'll need to be put into the same folder.

Here's a sample of the output. I'm going to put some kind of divider between frames, as everything tends to run together: http://pastebin.com/QLzU50pL