type Event a = [(Time, a)]
For example, an Event String can be a stream of chat users. ("Wellcome, " ++) <$> eusers
will create a stream of greetings from users who have entered the chat. type Behavior a = Time -> a
This type is well suited for game objects, the snake in our game will be Behavior. apply :: Behavior t (a -> b) -> Event ta -> Event tb apply bf ex = [(time, bf time x) | (time, x) <- ex]
As can be seen from this definition, apply applies the function inside Behavior to the Event, taking into account the time. module Snake where type Segment = (Int, Int) type Pos = (Int, Int) type Snake = [Segment]
One segment of the snake is a pair of coordinates, and the snake itself is a chain of these segments. Type Pos is needed only for convenience. startingSnake :: Snake startingSnake = [(10, 0), (11, 0), (12, 0)] wdth = 64 hdth = 48
Create the initial position of the snake and the constant for the size of the playing field. moveTo :: Pos -> Snake -> Snake moveTo hs = if h /= head s then h : init s else s keepMoving :: Snake -> Snake keepMoving s = let (x, y) = head s (x', y') = s !! 1 in moveTo (2*x - x', 2*y - y') s ifDied :: Snake -> Bool ifDied s@((x, y):_) = x<0 || x>=wdth || y<0 || y>=hdth || head s `elem` tail s
The moveTo function shifts the snake to the specified location, keepMoving continues to move, and ifDied checks to see if the snake has died from samoiding or colliding with boundaries. {-# LANGUAGE ScopedTypeVariables #-} import Control.Monad (when) import System.IO import System.Random import Graphics.UI.SDL as S hiding (flip) import Graphics.Rendering.OpenGL hiding (Rect, get) import Reactive.Banana as R import Data.Word (Word32) import Snake screenWidth = wdth*10 screenHeight = hdth*10 screenBpp = 32 ticks = 1000 `div` 20
screenWidth, screenHeight is the width and height of the screen, respectively; ticks is the number of milliseconds by which the frame will linger on the screen. main :: IO () main = withInit [InitEverything] $ do initScreen sources <- (,) <$> newAddHandler <*> newAddHandler network <- compile $ setupNetwork sources actuate network eventLoop sources network
In setupNetwork, a “network” will be built from Event and Behavior, compile will compile NetworkDescription into EventNetwork, and actuate will launch it. Events will be sent to the network from the eventLoop function, like signals to the brain from receptors. eventLoop :: (EventSource SDLKey, EventSource Word32) -> EventNetwork -> IO () eventLoop (essdl, estick) network = loop 0 Nothing where loop lt k = do s <- pollEvent t <- getTicks case s of (KeyDown (Keysym key _ _)) -> loop t (Just key) NoEvent -> do maybe (return ()) (fire essdl) k fire estick t loop t Nothing _ -> when (s /= Quit) (loop tk)
This is the “receptor” of our program. fire essdl - runs the essdl event, which contains the name of the key, if it is pressed at all. estick runs regardless of user behavior and carries the time since the start of the program. type EventSource a = (AddHandler a, a -> IO ()) addHandler :: EventSource a -> AddHandler a addHandler = fst fire :: EventSource a -> a -> IO () fire = snd
setupNetwork :: forall t. (EventSource SDLKey, EventSource Word32) -> NetworkDescription t () setupNetwork (essdl, estick) = do -- Keypress and tick events esdl <- fromAddHandler (addHandler essdl) etick <- fromAddHandler (addHandler estick)
First we get the Event from those timer and keyboard events that we launched in eventLoop. let ekey = filterE (flip elem [SDLK_DOWN, SDLK_UP, SDLK_LEFT, SDLK_RIGHT]) esdl moveSnake :: SDLKey -> Snake -> Snake moveSnake ks = case k of SDLK_UP -> moveTo (x, y-1) s SDLK_DOWN -> moveTo (x, y+1) s SDLK_LEFT -> moveTo (x-1, y) s SDLK_RIGHT -> moveTo (x+1, y) s where (x, y) = head s
Now create an event that means pressing the arrow - we do not need other keys. As you probably already guessed, filterE eliminates events that do not satisfy the predicate. moveSnake simply moves the snake, depending on the key pressed. brandom <- fromPoll randomFruits -- Snake let bsnake :: Behavior t Snake bsnake = accumB startingSnake $ (const startingSnake <$ edie) `union` (moveSnake <$> ekey) `union` (keepMoving <$ etick) `union` ((\s -> s ++ [last s]) <$ egot) edie = filterApply ((\s _ -> ifDied s) <$> bsnake) etick
fromPoll implements another way of interacting with the real world, but it is different from what we used before. First, we get Behavior, not Event. And secondly, the action in fromPoll should not be expensive. For example, it is good to use fromPoll along with variables. accumB :: a -> Event t (a -> a) -> Behavior ta
That is, roughly speaking, when an event occurs, the function inside it will be applied to the current value. accumB "x" [(time1,(++"y")),(time2,(++"z"))]
will create a Behavior, which at time1 will hold “xy”, and at time2 - “xyz”. filterApply :: Behavior t (a -> Bool) -> Event ta -> Event ta
This function discards events that do not satisfy the predicate inside Behavior. As the name suggests, this is something like filter + apply. -- Fruits bfruit :: Behavior t Pos bfruit = stepper (hdth `div` 2, wdth `div` 2) (brandom <@ egot) egot = filterApply ((\fsr _ -> elem fs && notElem rs) <$> bfruit <*> bsnake <*> brandom) etick
A new fruit with coordinates in brandom appears as soon as the snake has collected the current one. The combinator <@ "transfers" the contents of one Behavior to the Event, that is, in this case, the contents of the egot event will be replaced with a random coordinate from brandom. The new stepper function for us creates Behavior from Event and initial values, and its only difference from accumB is that the new Behavior event will not depend on the previous one. -- Counter ecount = accumE 0 $ ((+1) <$ egot) `union` ((const 0) <$ edie)
ecount is a scoring event. It is easy to guess that accumE creates an Event, not a Behavior. The counter will be incremented by one at the egot event, and zeroed at edie. let edraw = apply ((,,) <$> bsnake <*> bfruit) etick
edraw starts at every timer signal, and contains the current position of the snake and fruit. reactimate $ fmap drawScreen edraw reactimate $ fmap (flip setCaption [] . (++) "Snake. Points: " . show) ecount
The reactimate function launches an IO action from the Event. drawScreen draws the screen, and setCaption changes the name of the window. initScreen = do glSetAttribute glDoubleBuffer 1 screen <- setVideoMode screenWidth screenHeight screenBpp [OpenGL] setCaption "Snake. Points: 0" [] clearColor $= Color4 0 0 0 0 matrixMode $= Projection loadIdentity ortho 0 (fromIntegral screenWidth) (fromIntegral screenHeight) 0 (-1) 1 matrixMode $= Modelview 0 loadIdentity
randomFruits :: IO Pos randomFruits = (,) <$> (randomRIO (0, wdth-1)) <*> (randomRIO (0, hdth-1))
showSquare :: (GLfloat, GLfloat, GLfloat, GLfloat) -> Pos -> IO () showSquare (r, g, b, a) (x, y) = do -- Move to offset translate $ Vector3 (fromIntegral x*10 :: GLfloat) (fromIntegral y*10) 0 -- Start quad renderPrimitive Quads $ do -- Set color color $ Color4 rgba -- Draw square vertex $ Vertex3 (0 :: GLfloat) 0 0 vertex $ Vertex3 (10 :: GLfloat) 0 0 vertex $ Vertex3 (10 :: GLfloat) 10 0 vertex $ Vertex3 (0 :: GLfloat) 10 0 loadIdentity showFruit :: Pos -> IO () showFruit = showSquare (0, 1, 0, 1) showSnake :: Snake -> IO () showSnake = mapM_ (showSquare (1, 1, 1, 1)) drawScreen (s, f, t) = do clear [ColorBuffer] showSnake s showFruit f glSwapBuffers t' <- getTicks when ((t'-t) < ticks) (delay $ ticks - t' + t)
Source: https://habr.com/ru/post/140719/
All Articles