Tetris in Haskell

As a functional programming language, Haskell has some benefits, and some weaknesses. One such weakness is input and output. For my Computer Science 8 class at Dartmouth, my partner and I had to create a game of Tetris for our final project.

The Tetris game was based on some board libraries given to us as part of the assignment. The major tasks included creating pieces, stopping pieces from continually dropping when they hit the board or would go off the screen, making sure left and right moves and rotations would keep the piece entirely on the screen, and deciding when the game is over. In addition, we added on optional components of a pause button, a game over screen, and scoring.

Playing the GameLosing the Game

I am including here some snippits of the code from the assignment as I found creating Tetris in Haskell required some tricky things.

This code sample split down a defined “Region” into defined “Shapes” and then allowed coordinates to be taken from these for use in calculations. The code is limited to dealing with Rectangles and Polygons, and it eliminates duplicate coordinates to speed up later calculations.

-- Given a Region, returns a list of all vertices
vertR :: Region -> [(Float,Float)]
vertR (Shape s) = vertS s
vertR (Translate (u,v) r) = map (\(a,b) -> ((a+u),(b+v))) (vertR r)
vertR (RotateL r) = map (\(x,y) -> (-y,x)) (vertR r)
vertR (r1 `Union` r2) = nub (vertR r1 ++ vertR r2)
vertR Empty = []
-- Given a Rectangle or Polygon, returns the coordinates of the corners
vertS :: Shape -> [(Float,Float)]
vertS (Rectangle s1 s2) = [(-s1/2,s2/2),(s1/2,s2/2),(s1/2,-s2/2),(-s1/2,-s2/2)]
vertS (Polygon pts) = pts

Using this code, testing for collisions and piece movements was simplified:

-- Returns True if the given piece colides with the given Board at the x y specified
collide :: Board -> Float -> Float -> (Region,Region) -> Bool
collide board x y piece =
        (any (board `containsB`) (vertR (Translate (x,y) (snd piece))))
-- Move right if there is space
right :: (Float,Float) -> (Region, Region)-> Board-> Float
right (x,y) (p,ps) b = let tp = Translate (dx,0) p
                           tps = Translate (dx,0) ps
                           xs = map (+(x)) (map fst (vertR tp))
                           isOff = (filter (>(columns+0.5)) xs) /= []
                           isCol = (any (b `containsB`) (vertR (Translate (x,y) tps)))
                       in  if (isOff || isCol) then x else (x+dx)
-- Rotate if the piece will not go off the screen
rotateL :: (Float,Float) -> (Region,Region)-> Board -> (Region, Region)
rotateL (x,y) p b = let rp = RotateL (snd p)
                        xs = map (+(x)) (map fst (vertR rp))
                        isOff = ((filter (\x -> (x<(0.5)) || (x>(columns+0.5))) xs) /= [])
                        isCol = (any (b `containsB`) (vertR (Translate (x,y) rp)))
                    in  if (isOff || isCol) then p else ((RotateL (fst p)), (RotateL (snd p)))

Finally, the Maybe monad – which at first using Haskell seemed like an unneeded alternative for if-then statements – was used many times in this program. Using if-thens may have worked in some places, but the case of and pattern matching allowed by the Maybe type was very helpful. Here is one example, taken from a function which was waiting for a specific keypress to continue:

                       case e of Just (Key 'q' True) -> closeWindow (win uw)
                                 Just (Key 'r' True) -> do closeWindow (win uw)
                                                           main
                                 _                   -> waitForAction uw