#! /usr/bin/env runhaskell {- Copyright 2010 Dino Morelli Released under BSD3 Simple curses aquarium written in Haskell to learn about the hscurses library. Number of fish can be given as an argument, defaults to 20 This code lives here: http://ui3.info/darcs/hscurses-fish-ex/ Inspired by this C code: http://alexeyt.freeshell.org/code/aquarium.c -} import Control.Concurrent ( threadDelay ) import Control.Concurrent.MVar ( MVar, newMVar, putMVar, readMVar, takeMVar ) import Control.Monad ( liftM, mplus, replicateM ) import Data.Char ( ord ) import Data.Maybe ( fromJust ) import Safe ( headMay, readMay ) import System.Environment ( getArgs ) import System.Posix.Signals ( Handler (Catch), installHandler, sigINT, sigTERM ) import System.Random ( randomRIO ) import UI.HSCurses.Curses ( Pair (..), attr0, attrSet, endWin, initCurses, initPair, mvAddCh , mvWAddStr, refresh, scrSize, startColor, stdScr, wMove ) import UI.HSCurses.CursesHelper ( black, blue, cyan, green, magenta, red, white, yellow ) import UI.HSCurses.Widgets ( getWidth ) {- The data needed to track each fish -} data Fish = Fish Int -- y Int -- x Pair -- color Bool -- fish is facing right interruptHandler :: MVar Bool -> IO () interruptHandler mvRunStatus = do -- We don't care what it is, we just want to take it from everyone else _ <- takeMVar mvRunStatus -- Make note in the state that the user wants to quit putMVar mvRunStatus True worker :: MVar Bool -> [Fish] -> IO () worker mvRunStatus priorSchool = do -- Update fish positions school <- mapM swim priorSchool -- Draw the fish mapM_ drawFish school refresh threadDelay 100000 -- Pull the run state.. stopNow <- readMVar mvRunStatus -- examine it to figure out what to do next if stopNow == True then return () else worker mvRunStatus school {- Construct a fish on a random row, facing a random direction Sets the starting column to be just offscreen -} spawn :: IO Fish spawn = do (maxRows, maxCols) <- scrSize y <- randomRIO (0, maxRows - 1) right <- randomRIO (True, False) let x = case right of True -> -1 False -> maxCols color <- liftM Pair $ randomRIO (1, 7) return $ Fish y x color right {- Returns a copy of a fish moved to a random column onscreen Used for first-time fish initialization -} randomXPos :: Fish -> IO Fish randomXPos (Fish y _ color right) = do maxCols <- liftM getWidth scrSize newX <- randomRIO (0, maxCols - 1) return $ Fish y newX color right {- Move a fish forward. If it's fully offscreen, spawn a random new fish to replace it. -} swim :: Fish -> IO Fish -- Right-facing fish swim (Fish y x color right@True) = do let modFish = Fish y (x + 1) color right maxCols <- liftM getWidth scrSize if x > (maxCols + 2) then spawn else return modFish -- Left-facing fish swim (Fish y x color right@False) = do let modFish = Fish y (x - 1) color right if x < -3 then spawn else return modFish {- Draw a fish The reason for this complicated draw-each-char is that mvWAddStr crashes if part of the string is past the bottom right corner of the window. Not so with calling mvAddCh even with out-of-bounds coords. -} drawPart :: Int -> (Int, Char) -> IO () drawPart y (x, c) = do mvAddCh y x $ fromIntegral . ord $ c drawFish :: Fish -> IO () -- Right-facing fish drawFish (Fish y x color True) = do attrSet attr0 color mapM_ (drawPart y) $ zip [(x - 3) .. x] " ><>" wMove stdScr 0 0 -- Left-facing fish drawFish (Fish y x color False) = do attrSet attr0 color mapM_ (drawPart y) $ zip [x .. (x + 3)] "<>< " wMove stdScr 0 0 {- Clear the screen by drawing spaces with the given color's background -} paintEntireScr :: Pair -> IO () paintEntireScr color = do (maxRows, maxCols) <- scrSize let blanks = replicate (maxCols - 1) ' ' attrSet attr0 color mapM_ (\y -> mvWAddStr stdScr y 0 blanks) [0 .. (maxRows - 1)] refresh main :: IO () main = do -- Get number of fish from the command-line, or use a default value args <- getArgs let nfish = fromJust $ (return args >>= headMay >>= readMay) `mplus` return 20 initCurses -- Color initialization startColor initPair (Pair 1) green black initPair (Pair 2) red black initPair (Pair 3) yellow black initPair (Pair 4) blue black initPair (Pair 5) magenta black initPair (Pair 6) cyan black initPair (Pair 7) white black paintEntireScr $ Pair 7 -- Randomly generate the first school of fish school <- mapM randomXPos =<< replicateM nfish spawn -- Data for communicating between threads mvRunStatus <- newMVar False -- A map to install the same handler for multiple signals mapM_ ( \signal -> installHandler signal (Catch $ interruptHandler mvRunStatus) Nothing ) [sigINT, sigTERM] -- This starts the endless work loop worker mvRunStatus school paintEntireScr $ Pair 7 endWin