5
\$\begingroup\$

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
import Control.Concurrent (threadDelay)

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,
                                    frameNullFill :: String,
                                    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
pad _ [] = []
pad padLen (x : xs)
    | length x > padLen = take padLen x ++ pad padLen xs
    | otherwise = x ++ (replicate (padLen - length x) ' ') ++ pad padLen xs

addMSDelay :: Int -> IO ()
addMSDelay ms
    | 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
    addMSDelay delay
    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

\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

For someone who's not currently using Haskell, the code is pretty readable and modular. You might consider avoiding tuples for types like Frame as that may bite you in the future, and it's a bit hard to follow. Also, your post almost looks like literate Haskell, have you considered that?

\$\endgroup\$
2
  • 1
    \$\begingroup\$ I used tuples because I found them to be slightly cleaner to pattern match against, although, I see your point. And what do you mean I'm your last sentence? \$\endgroup\$ Sep 30, 2014 at 14:54
  • \$\begingroup\$ Ben, Add more information. \$\endgroup\$ Sep 30, 2014 at 15:02

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.