gridland-0.1.0.1: src/GridLand.hs
module GridLand
( Config(..)
, Color(..)
, Stretch(..)
, ColorFilter(..)
, Angle(..)
, Backdrop(..)
, Input(..)
, Key(..)
, KeyState(..)
, Location(..)
, ToSprite(..)
, BackdropImage
, Sprite
, Sfx
, Music
, GridLand
, titleBar
, loadSprite
, loadSpriteStretch
, loadBackdropImage
, loadBackdropImageStretch
, drawSpriteFront
, drawSpriteMiddle
, drawSpriteBack
, loadMusic
, playMusic
, stopMusic
, stopAllMusic
, loadSfx
, playSfx
, print'
, putStrLn'
, runGridLand
, backdrop
, getData
, putData
, modifyData
, getPlayingMusic
, io
, getInputs
, getMousePosition
) where
-- base
import Debug.Trace
import Data.IORef
import Data.Char
import Data.Maybe
import Control.Arrow
-- SDL
import qualified Graphics.UI.SDL as SDL
import qualified Graphics.UI.SDL.Video as SDL
-- SDL-image
import qualified Graphics.UI.SDL.Image as Image
-- SDL-ttf (unused)
-- import qualified Graphics.UI.SDL.TTF as TTF
-- SDL-mixer
import qualified Graphics.UI.SDL.Mixer as Mixer
-- SDL-gfx
import qualified Graphics.UI.SDL.Framerate as Gfx
import qualified Graphics.UI.SDL.Primitives as Gfx
import qualified Graphics.UI.SDL.Rotozoomer as Gfx
-- containers
import qualified Data.Map as Map
import qualified Data.Set as Set
-- grid
import qualified Math.Geometry.Grid as Grid
-- vector
import qualified Data.Vector as V
-- mtl
import qualified Control.Monad.State as State
import qualified Control.Monad.RWS as RWS
-- array
import qualified Data.Array as Array
-- safe
import qualified Safe as Safe
import GridLand.Import
import GridLand.Data
newTodo :: Todo
newTodo = Todo Map.empty Map.empty Map.empty (BkdColor White)
getData :: GridLand a a
getData = RWS.gets snd
putData :: a -> GridLand a ()
putData a = RWS.modify . second $ const a
modifyData :: (a -> a) -> GridLand a ()
modifyData f = RWS.modify . second $ f
spriteDegrees :: [Int]
spriteDegrees = [ rot * 360 `div` rotations | rot <- [0 .. (rotations - 1)]]
spriteOpts :: [(ColorFilter, Int)]
spriteOpts = noFilters ++ filters
where
noFilters = [(NoFilter, theta) | theta <- spriteDegrees]
filters = [(cf c, theta) | cf <- [Tint, Replace], c <- [minBound..maxBound], theta <- spriteDegrees]
setColorKey :: (Word8, Word8, Word8) -> SDL.Surface -> IO SDL.Surface
setColorKey (r, g, b) sur = mapRGB sur r g b >>= SDL.setColorKey sur [SDL.SrcColorKey] >> return sur
mapRGB :: SDL.Surface -> Word8 -> Word8 -> Word8 -> IO SDL.Pixel
mapRGB = SDL.mapRGB . SDL.surfaceGetPixelFormat
colorToPixel :: Color -> SDL.Pixel
colorToPixel color = let
(r',g,b') = colorValue color
(r,b) = (b',r')
v = shiftL 0xff (8 * 3) .|. shiftL (fromIntegral b) (8 * 2) .|. shiftL (fromIntegral g) (8 * 1) .|. (fromIntegral r)
in SDL.Pixel v
loadSpriteStretch :: FilePath -> Stretch -> GridLand a Sprite
loadSpriteStretch path stretch = do
base <- liftIO $ Image.load path >>= SDL.displayFormat >>= setColorKey (0xff,0x00,0xff)
let (w,h) = (SDL.surfaceGetWidth base, SDL.surfaceGetHeight base)
let side = max w h
ts <- RWS.asks tileSize
let zoom = (fromIntegral ts) / (fromIntegral side)
let blit (cf, theta) = do
rotozoom <- Gfx.rotozoom base (fromIntegral theta) zoom (stretch == Smooth)
ping <- SDL.displayFormat rotozoom >>= setColorKey (0xff, 0x00, 0xff)
pong <- setColorKey (0xff, 0x00, 0xff) rotozoom
let (w,h) = (SDL.surfaceGetWidth rotozoom, SDL.surfaceGetHeight rotozoom)
let fmt = SDL.surfaceGetPixelFormat rotozoom
let cfFn v = case cf of
NoFilter -> SDL.Pixel v
Tint color -> let
(tr, tg, tb) = colorValue color
(cr, cg, cb) = fromColor32 v
(r, g, b) = (shiftR tr 1 + shiftR cr 1, shiftR tg 1 + shiftR cg 1, shiftR tb 1 + shiftR cb 1)
tinted = toColor32' r g b
in SDL.Pixel $ if 0x00ff00ff == v .&. 0x00ffffff
then 0x00ff00ff
else tinted
Replace color -> let
(SDL.Pixel p) = colorToPixel color
in SDL.Pixel $ if 0x00ff00ff == v .&. 0x00ffffff
then 0x00ff00ff
else p
forM_ ([(x,y) | x <- [0 .. w - 1], y <- [0 .. h - 1]]) $ \(x,y) -> do
(SDL.Pixel v) <- getPixel32 x y ping
putPixel32 x y (cfFn v) pong
SDL.freeSurface ping
return pong
frames <- liftIO $ mapM blit spriteOpts
liftIO $ SDL.freeSurface base
let spriteFrames = V.fromListN totalFrames frames
key <- RWS.gets (Map.size . sprites . fst)
let spr = Sprite key
RWS.modify . first $ \s -> s { sprites = Map.insert spr spriteFrames (sprites s) }
return spr
loadSprite :: FilePath -> GridLand a Sprite
loadSprite path = loadSpriteStretch path Pixelated
copySurface :: SDL.Surface -> IO SDL.Surface
copySurface sur = do
copy <- copyFromSurface sur
SDL.blitSurface sur Nothing copy Nothing
return copy
copyFromSurface :: SDL.Surface -> IO SDL.Surface
copyFromSurface sur = do
let fmt = SDL.surfaceGetPixelFormat sur
bpp <- fromIntegral `liftM` SDL.pixelFormatGetBitsPerPixel fmt
rMask <- pixelFormatGetRMask fmt
gMask <- pixelFormatGetGMask fmt
bMask <- pixelFormatGetBMask fmt
flags <- SDL.surfaceGetFlags sur
aMask <- if elem SDL.SrcColorKey flags then return 0 else pixelFormatGetAMask fmt
SDL.createRGBSurface [SDL.SWSurface] (SDL.surfaceGetWidth sur) (SDL.surfaceGetHeight sur) bpp rMask gMask bMask aMask
pixelFormatGetRMask :: SDL.PixelFormat -> IO Word32
pixelFormatGetRMask format = withForeignPtr format $ \hsc_ptr -> peekByteOff hsc_ptr 16
pixelFormatGetGMask :: SDL.PixelFormat -> IO Word32
pixelFormatGetGMask format = withForeignPtr format $ \hsc_ptr -> peekByteOff hsc_ptr 20
pixelFormatGetBMask :: SDL.PixelFormat -> IO Word32
pixelFormatGetBMask format = withForeignPtr format $ \hsc_ptr -> peekByteOff hsc_ptr 24
pixelFormatGetAMask :: SDL.PixelFormat -> IO Word32
pixelFormatGetAMask format = withForeignPtr format $ \hsc_ptr -> peekByteOff hsc_ptr 28
getPixel32 :: Int -> Int -> SDL.Surface -> IO SDL.Pixel
getPixel32 x y s = do
pixels <- castPtr `liftM` SDL.surfaceGetPixels s
SDL.Pixel `liftM` peekElemOff pixels ((y * SDL.surfaceGetWidth s) + x)
putPixel32 :: Int -> Int -> SDL.Pixel -> SDL.Surface -> IO ()
putPixel32 x y (SDL.Pixel pixel) s = do
pixels <- castPtr `liftM` SDL.surfaceGetPixels s
pokeElemOff pixels ((y * SDL.surfaceGetWidth s) + x) pixel
loadBackdropImageStretch :: FilePath -> Stretch -> GridLand a Backdrop
loadBackdropImageStretch path stretch = do
base <- liftIO $ Image.load path
scr <- RWS.asks screen
let (scrW,scrH) = (SDL.surfaceGetWidth scr, SDL.surfaceGetHeight scr)
let (w,h) = (SDL.surfaceGetWidth base, SDL.surfaceGetHeight base)
let (zoomW, zoomH) = (fromIntegral scrW / fromIntegral w, fromIntegral scrH / fromIntegral h)
zoomed <- liftIO $ Gfx.zoom base zoomW zoomH (stretch == Smooth)
key <- RWS.gets (Map.size . bkdImages . fst)
let bi = BackdropImage key
RWS.modify . first $ \s -> s { bkdImages = Map.insert bi zoomed (bkdImages s) }
liftIO $ SDL.freeSurface base
return (BkdImage bi)
loadBackdropImage :: FilePath -> GridLand a Backdrop
loadBackdropImage path = loadBackdropImageStretch path Smooth
pollEvents :: IO [SDL.Event]
pollEvents = do
event <- SDL.pollEvent
if event == SDL.NoEvent
then return []
else (:) <$> return event <*> pollEvents
toMousePosition :: Foundation -> (Word16, Word16) -> Location
toMousePosition Foundation{..} (x,y) = Location {
locX = fromIntegral x `div` tileSize,
locY = fromIntegral y `div` tileSize
}
pollInputs :: Foundation -> IO ([Input], Maybe Location)
pollInputs foundation = do
events <- pollEvents
return (foldr cvt [] events, mousePos events)
where
mousePos = Safe.headMay . catMaybes . map mpos
mpos (SDL.MouseMotion x y _ _) = let
pos = toMousePosition foundation (x,y)
in if inRange foundation pos
then Just pos
else Nothing
mpos _ = Nothing
-- Quit
cvt SDL.Quit inputs = Quit : inputs
cvt (SDL.KeyDown (SDL.Keysym keysym _ ch)) inputs = case keysym of
SDL.SDLK_ESCAPE -> Quit : inputs
-- Key (pressed)
SDL.SDLK_UP -> pressed UpArrow : inputs
SDL.SDLK_DOWN -> pressed DownArrow : inputs
SDL.SDLK_LEFT -> pressed LeftArrow : inputs
SDL.SDLK_RIGHT -> pressed RightArrow : inputs
SDL.SDLK_RETURN -> pressed Enter : inputs
SDL.SDLK_LSHIFT -> pressed Shift : inputs
SDL.SDLK_RSHIFT -> pressed Shift : inputs
SDL.SDLK_LCTRL -> pressed Ctrl : inputs
SDL.SDLK_RCTRL -> pressed Ctrl : inputs
SDL.SDLK_LALT -> pressed AltKey : inputs
SDL.SDLK_RALT -> pressed AltKey : inputs
SDL.SDLK_TAB-> pressed Tab : inputs
SDL.SDLK_BACKSPACE -> pressed Backspace : inputs
SDL.SDLK_LSUPER -> pressed Meta : inputs
SDL.SDLK_RSUPER -> pressed Meta : inputs
key ->
if (key >= SDL.SDLK_SPACE && key <= SDL.SDLK_z)
then pressed (Char $ toLower ch) : inputs
else inputs -- ignore
-- Key (released)
cvt (SDL.KeyUp (SDL.Keysym keysym _ ch)) inputs = case keysym of
SDL.SDLK_UP -> released UpArrow : inputs
SDL.SDLK_DOWN -> released DownArrow : inputs
SDL.SDLK_LEFT -> released LeftArrow : inputs
SDL.SDLK_RIGHT -> released RightArrow : inputs
SDL.SDLK_RETURN -> released Enter : inputs
SDL.SDLK_LSHIFT -> released Shift : inputs
SDL.SDLK_RSHIFT -> released Shift : inputs
SDL.SDLK_LCTRL -> released Ctrl : inputs
SDL.SDLK_RCTRL -> released Ctrl : inputs
SDL.SDLK_LALT -> released AltKey : inputs
SDL.SDLK_RALT -> released AltKey : inputs
SDL.SDLK_TAB-> released Tab : inputs
SDL.SDLK_BACKSPACE -> released Backspace : inputs
SDL.SDLK_LSUPER -> released Meta : inputs
SDL.SDLK_RSUPER -> released Meta : inputs
key ->
if (key >= SDL.SDLK_SPACE && key <= SDL.SDLK_z)
then released (Char $ toLower ch) : inputs
else inputs -- ignore
-- Click
cvt (SDL.MouseButtonDown x y SDL.ButtonLeft) inputs = Click Location{ locX = fromIntegral x, locY = fromIntegral y } : inputs
-- Ignore the rest
cvt _ inputs = inputs
mergeInputs :: [Input] -> [Input] -> [Input]
mergeInputs old new = new
stepInputs :: [Input] -> [Input]
stepInputs = foldr step []
where
held key = Key key Held
step inp inps = case inp of
Key key Pressed -> held key : inps
Key key Released -> inps
_ -> inp : inps
getInputs :: GridLand a [Input]
getInputs = State.gets (inputs . fst)
getMousePosition :: GridLand a Location
getMousePosition = State.gets (mousePosition . fst)
pressed :: Key -> Input
pressed = flip Key Pressed
released :: Key -> Input
released = flip Key Released
fromColor32 :: Word32 -> (Word8, Word8, Word8)
fromColor32 c =
(fromIntegral ((shiftR c (2 * 8)) .&. 0xff),
fromIntegral ((shiftR c 8) .&. 0xff),
fromIntegral (c .&. 0xff))
fromColor32' :: Word32 -> (Word8, Word8, Word8)
fromColor32' c =
(fromIntegral (c .&. 0xff),
fromIntegral ((shiftR c 8) .&. 0xff),
fromIntegral ((shiftR c (2 * 8)) .&. 0xff))
toColor32 :: Word8 -> Word8 -> Word8 -> Word32
toColor32 r g b =
shiftL 0xff (3 * 8) .|.
shiftL (fromIntegral b) (2 * 8) .|.
shiftL (fromIntegral g) 8 .|.
(fromIntegral r)
toColor32' :: Word8 -> Word8 -> Word8 -> Word32
toColor32' r g b =
shiftL 0xff (3 * 8) .|.
shiftL (fromIntegral r) (2 * 8) .|.
shiftL (fromIntegral g) 8 .|.
(fromIntegral b)
colorValue :: Integral a => Color -> (a, a, a)
colorValue = \case
Red -> (0xcc, 0x00, 0x00)
Orange -> (0xff, 0xa5, 0x00)
Yellow -> (0xff, 0xff, 0x00)
YellowGreen -> (0x7f, 0xff, 0x00)
Green -> (0x00, 0x66, 0x00)
LightBlue -> (0x66, 0xff, 0xff)
Blue -> (0x00, 0x00, 0x99)
Pink -> (0xff, 0x66, 0x99)
Black -> (0x00, 0x00, 0x00)
White -> (0xff, 0xff, 0xff)
colorValue' :: Integral a => Color -> (a -> a -> a -> b) -> b
colorValue' c f = uncurryN f (colorValue c)
rotations, colors, withoutColors, withColors, totalFrames, colorAngleInterval:: Int
rotations = 36
colors = 1 + fromEnum (maxBound :: Color)
withColors = 2
withoutColors = 1
totalFrames = (withoutColors + withColors * colors) * rotations
colorAngleInterval = colors * rotations
colorAngleOffset :: Color -> Angle -> Int
colorAngleOffset c t = colorOffset c + angleOffset t
angleOffset :: Angle -> Int
angleOffset (Radians r) = angleOffset $ Degrees (round $ 180 * r / pi)
angleOffset (Degrees d) = ((d `mod` 360) * rotations `div` 360)
colorOffset :: Color -> Int
colorOffset c = fromEnum c * rotations
frameOffset :: ColorFilter -> Angle -> Int
frameOffset NoFilter theta = angleOffset theta
frameOffset (Tint c) theta = rotations + colorAngleOffset c theta
frameOffset (Replace c) theta = rotations + colorAngleInterval + colorAngleOffset c theta
spriteGfx :: Sprite -> ColorFilter -> Location -> Angle -> GridLand a Gfx
spriteGfx spr cf loc theta = do
let offset = frameOffset cf theta
sprMap <- RWS.gets (sprites . fst)
let sur = (sprMap Map.! spr) V.! offset
return $ Gfx sur loc
gfxRect :: Int -> Location -> SDL.Surface -> SDL.Rect
gfxRect ts Location{..} sur = let
(w, h) = (SDL.surfaceGetWidth sur, SDL.surfaceGetHeight sur)
in SDL.Rect (ts * locX) (ts * locY) ts ts
drawSpriteMapFront :: ToSprite s => Map Location s -> GridLand a ()
drawSpriteMapFront sprMap = do
return ()
drawSpriteFront :: ToSprite s => s -> ColorFilter -> Location -> Angle -> GridLand a ()
drawSpriteFront a cf loc theta = do
gfx <- spriteGfx (toSprite a) cf loc theta
RWS.tell $ mempty { todoFrontSprites = Map.singleton loc gfx }
drawSpriteMiddle :: ToSprite s => s -> ColorFilter -> Location -> Angle -> GridLand a ()
drawSpriteMiddle a cf loc theta = do
gfx <- spriteGfx (toSprite a) cf loc theta
RWS.tell $ mempty { todoMiddleSprites = Map.singleton loc gfx }
drawSpriteBack :: ToSprite s => s -> ColorFilter -> Location -> Angle -> GridLand a ()
drawSpriteBack a cf loc theta = do
gfx <- spriteGfx (toSprite a) cf loc theta
RWS.tell $ mempty { todoBackSprites = Map.singleton loc gfx }
drawSprite :: ToSprite s => s -> ColorFilter -> Location -> Angle -> GridLand a ()
drawSprite a cf loc theta = do
(s, ts) <- RWS.asks (screen &&& tileSize)
Gfx{..} <- spriteGfx (toSprite a) cf loc theta
let (w, h) = (SDL.surfaceGetWidth gfxSurface, SDL.surfaceGetHeight gfxSurface)
let tileRect = Just $ SDL.Rect ((w - ts) `div` 2) ((h - ts) `div` 2) ts ts
void . liftIO $ SDL.blitSurface gfxSurface tileRect s (Just $ gfxRect ts gfxLocation gfxSurface)
drawGfx :: SDL.Surface -> Int -> Gfx -> IO ()
drawGfx scr ts Gfx{..} = do
let (w, h) = (SDL.surfaceGetWidth gfxSurface, SDL.surfaceGetHeight gfxSurface)
let tileRect = Just $ SDL.Rect ((w - ts) `div` 2) ((h - ts) `div` 2) ts ts
void $ SDL.blitSurface gfxSurface tileRect scr (Just $ gfxRect ts gfxLocation gfxSurface)
loadMusic :: FilePath -> GridLand a Music
loadMusic (path) = do
mus <- liftIO $ Mixer.loadMUS path
key <- RWS.gets (Map.size . musics . fst)
let music = Music key
RWS.modify . first $ \s -> s { musics = Map.insert music mus (musics s) }
return music
playMusic :: Music -> Maybe Int -> GridLand a ()
playMusic music mloops = do
mus <- RWS.gets ((Map.! music) . musics . fst)
let loops = maybe (-1) (\n -> if n < -1 then 0 else n) mloops
liftIO $ Mixer.playMusic mus loops
RWS.modify . first $ (\s -> s { playingMusic = Just music } )
stopMusic :: Music -> GridLand a ()
stopMusic music = do
mplaying <- RWS.gets (playingMusic . fst)
case mplaying of
Nothing -> return ()
Just currMusic -> do
when (music == currMusic) $ do
liftIO Mixer.haltMusic
RWS.modify . first $ (\s -> s { playingMusic = Nothing } )
stopAllMusic :: GridLand a ()
stopAllMusic = do
liftIO Mixer.haltMusic
RWS.modify . first $ (\s -> s { playingMusic = Nothing } )
getPlayingMusic :: GridLand a (Maybe Music)
getPlayingMusic = RWS.gets (playingMusic . fst)
establishPlayingMusic :: GridLand a ()
establishPlayingMusic = do
isPlaying <- liftIO Mixer.playingMusic
unless isPlaying $ RWS.modify . first $ (\s -> s { playingMusic = Nothing } )
loadSfx :: FilePath -> GridLand a Sfx
loadSfx path = do
sfxRef <- liftIO $ Mixer.loadWAV path
key <- RWS.gets (Map.size . sfxs . fst)
let sfx = Sfx key
RWS.modify . first $ \s -> s { sfxs = Map.insert sfx sfxRef (sfxs s) }
return sfx
playSfx :: Sfx -> GridLand a ()
playSfx sfx = do
chunk <- RWS.gets ((Map.! sfx) . sfxs . fst)
chan <- RWS.gets (currSfxChan . fst)
void . liftIO $ Mixer.playChannel chan chunk 0
RWS.modify . first $ \s -> s { currSfxChan = mod (chan + 1) sfxChannels }
sfxChannels :: Int
sfxChannels = 16
-- | Config -> Start -> Update -> End -> IO ()
runGridLand :: Config -> GridLand () a -> GridLand a () -> GridLand a () -> IO ()
runGridLand cfg onStart onUpdate onEnd = do
foundation@Foundation{..} <- start cfg
let common = newCommon
(initUserData, (initCommon,_), _) <- RWS.runRWST (unGridLand onStart) foundation (common, ())
let initState = (initCommon, initUserData)
let update = establishPlayingMusic >> onUpdate >> drawBackdrop
let dgfx = drawGfx screen tileSize
endState <- ($ initState) $ fix $ \loop state -> do
startTick <- SDL.getTicks
let inps = (inputs . fst) state
let pos = (mousePosition . fst) state
(inps', mpos) <- first (mergeInputs (stepInputs inps)) <$> pollInputs foundation
let state' = first (\s -> s {
inputs = inps',
mousePosition = fromMaybe (mousePosition s) mpos
}) state
(continue, state'', todo) <- RWS.runRWST (unGridLand update) foundation state'
let ranger loc _ = inRange foundation loc
mapM_ dgfx (Map.elems . Map.filterWithKey ranger $ todoBackSprites todo)
mapM_ dgfx (Map.elems . Map.filterWithKey ranger $ todoMiddleSprites todo)
mapM_ dgfx (Map.elems . Map.filterWithKey ranger $ todoFrontSprites todo)
liftIO $ SDL.flip screen
endTick <- SDL.getTicks
let diff = endTick - startTick
when (diff < 16) (SDL.delay $ 16 - diff)
if elem Quit inps'
then return state''
else loop state''
void $ RWS.execRWST (unGridLand onEnd) foundation endState
end foundation common
inRange :: Foundation -> Location -> Bool
inRange Foundation{..} Location{..} = locX >= 0 && locY >= 0 && locX < cols && locY < rows
start :: Config -> IO Foundation
start Config{..} = do
SDL.init [SDL.InitEverything]
screen <- SDL.setVideoMode (cfgCols * cfgTileSize) (cfgRows * cfgTileSize) 32 [SDL.SWSurface]
SDL.setCaption "Grid Land" ""
SDL.enableUnicode True
colorMap <- getColorMap screen
-- ttfOk <- TTF.init
Mixer.openAudio 22050 Mixer.AudioS16Sys 2 4096
Mixer.allocateChannels sfxChannels
return $ Foundation screen colorMap cfgRows cfgCols cfgTileSize
getColorMap :: SDL.Surface -> IO (Color -> SDL.Pixel)
getColorMap s = do
let fmt = SDL.surfaceGetPixelFormat s
pixels <- forM [minBound..maxBound] $ \c -> liftIO $ colorValue' c (SDL.mapRGB fmt)
let table = V.fromList pixels
return $ \c -> table V.! (fromEnum c)
newCommon :: Common
newCommon = Common Map.empty Map.empty Map.empty Map.empty (BkdColor White) Nothing [] (Location 0 0) 0
end :: Foundation -> Common -> IO ()
end Foundation{..} Common{..} = do
mapM_ SDL.freeSurface (Map.elems bkdImages)
mapM_ (mapM_ SDL.freeSurface . V.toList) (Map.elems sprites)
mapM_ Mixer.freeMusic (Map.elems musics)
mapM_ (const $ return ()) (Map.elems sfxs) -- Mixer.freeChunks is missing its binding
SDL.freeSurface screen
Mixer.closeAudio
-- TTF.quit
SDL.quit
putStrLn' :: String -> GridLand a ()
putStrLn' = liftIO . putStrLn
print' :: Show b => b -> GridLand a ()
print' = liftIO . print
titleBar :: String -> GridLand a ()
titleBar title = liftIO $ SDL.setCaption title ""
backdrop :: Backdrop -> GridLand a ()
backdrop b = RWS.modify . first $ \s -> s { currBkd = b }
drawBackdrop :: GridLand a ()
drawBackdrop = do
(s, cmap) <- RWS.asks (screen &&& colorMap)
b <- RWS.gets (currBkd . fst)
case b of
BkdColor c -> void . liftIO $ SDL.fillRect s Nothing (cmap c)
BkdImage bi -> do
img <- RWS.gets ((Map.! bi) . bkdImages . fst)
void . liftIO $ SDL.blitSurface img Nothing s Nothing
io :: IO b -> GridLand a b
io = liftIO