diff --git a/TODO.md b/TODO.md index 6d90b38..d885871 100644 --- a/TODO.md +++ b/TODO.md @@ -8,3 +8,8 @@ * Check cabal outdated * Check in diffs between solution and problem and make sure it is stable (such that every fix in a solution gets propagated to the problem and vice versa) + +## UI + +* square that rotates with time +* paint, clear, paintAll: Maybe more complicated program with several paint calls, picture builds up if you forget to call clear diff --git a/cabal.project b/cabal.project index 492ec46..dcab037 100644 --- a/cabal.project +++ b/cabal.project @@ -1,2 +1,5 @@ test-show-details: direct packages: rhine-koans.cabal + ../rhine/rhine-gloss/rhine-gloss.cabal + ../rhine/rhine/rhine.cabal + ../rhine/automaton/automaton.cabal diff --git a/generic/test-gloss/TestGloss.hs b/generic/test-gloss/TestGloss.hs new file mode 100644 index 0000000..65ec1f7 --- /dev/null +++ b/generic/test-gloss/TestGloss.hs @@ -0,0 +1,34 @@ +module TestGloss where + +-- base +import Control.Concurrent +import Control.Monad +import Data.IORef +import System.Exit + +-- rhine-gloss +import FRP.Rhine.Gloss + +expectPic :: Picture -> [Picture] -> IO () +expectPic received expected = + let flattened = flattenPictures received + in if flattened == expected + then putStrLn "Well done!" + else do + putStrLn $ "Expected: " ++ show expected + putStrLn $ "Received: " ++ show flattened + exitFailure + +flattenPictures :: Picture -> [Picture] +flattenPictures (Pictures ps) = ps >>= flattenPictures +flattenPictures Blank = [] +flattenPictures picture = [picture] + +stepGlossRhine :: Rhine (GlossConcT IO) GlossSimClockIO () () -> [Float] -> IO [Picture] +stepGlossRhine rhine timestamps = do + vars <- makeGlossEnv + void $ forkIO $ runGlossConcT (flow rhine) vars + forM timestamps $ \timestamp -> do + putMVar (timeVar vars) timestamp + threadDelay 100000 + readIORef (picRef vars) diff --git a/hie.yaml b/hie.yaml new file mode 100644 index 0000000..fb0cd9b --- /dev/null +++ b/hie.yaml @@ -0,0 +1,256 @@ +cradle: + cabal: + - path: "generic/test-io" + component: "rhine-koans:lib:test-io" + + - path: "./Main.hs" + component: "rhine-koans:exe:basic-1-1-hello-rhine" + + - path: "koans/basic/1/1-hello-rhine/test" + component: "rhine-koans:test:basic-1-1-hello-rhine-test" + + - path: "./Main.hs" + component: "rhine-koans:exe:basic-1-2-fix-the-bug" + + - path: "koans/basic/1/2-fix-the-bug/test" + component: "rhine-koans:test:basic-1-2-fix-the-bug-test" + + - path: "./Main.hs" + component: "rhine-koans:exe:basic-1-3-faster" + + - path: "koans/basic/1/3-faster/test" + component: "rhine-koans:test:basic-1-3-faster-test" + + - path: "./Main.hs" + component: "rhine-koans:exe:basic-1-4-compose" + + - path: "koans/basic/1/4-compose/test" + component: "rhine-koans:test:basic-1-4-compose-test" + + - path: "./Main.hs" + component: "rhine-koans:exe:basic-1-5-compose-more" + + - path: "koans/basic/1/5-compose-more/test" + component: "rhine-koans:test:basic-1-5-compose-more-test" + + - path: "./Main.hs" + component: "rhine-koans:exe:basic-1-6-compose-signal-functions-and-clocks" + + - path: "koans/basic/1/6-compose-signal-functions-and-clocks/test" + component: "rhine-koans:test:basic-1-6-compose-signal-functions-and-clocks-test" + + - path: "./Main.hs" + component: "rhine-koans:exe:basic-1-7-compose-on-the-same-clock" + + - path: "koans/basic/1/7-compose-on-the-same-clock/test" + component: "rhine-koans:test:basic-1-7-compose-on-the-same-clock-test" + + - path: "./Main.hs" + component: "rhine-koans:exe:basic-1-8-compose-on-different-clocks" + + - path: "koans/basic/1/8-compose-on-different-clocks/test" + component: "rhine-koans:test:basic-1-8-compose-on-different-clocks-test" + + - path: "./Main.hs" + component: "rhine-koans:exe:basic-2-1-input" + + - path: "koans/basic/2/1-input/test" + component: "rhine-koans:test:basic-2-1-input-test" + + - path: "./Main.hs" + component: "rhine-koans:exe:basic-2-2-count-the-words" + + - path: "koans/basic/2/2-count-the-words/test" + component: "rhine-koans:test:basic-2-2-count-the-words-test" + + - path: "./Main.hs" + component: "rhine-koans:exe:basic-2-3-count-the-lines" + + - path: "koans/basic/2/3-count-the-lines/test" + component: "rhine-koans:test:basic-2-3-count-the-lines-test" + + - path: "./Main.hs" + component: "rhine-koans:exe:basic-2-4-count-all-the-words" + + - path: "koans/basic/2/4-count-all-the-words/test" + component: "rhine-koans:test:basic-2-4-count-all-the-words-test" + + - path: "./Main.hs" + component: "rhine-koans:exe:basic-2-5-count-all-the-chars" + + - path: "koans/basic/2/5-count-all-the-chars/test" + component: "rhine-koans:test:basic-2-5-count-all-the-chars-test" + + - path: "./Main.hs" + component: "rhine-koans:exe:basic-2-6-count-everything" + + - path: "koans/basic/2/6-count-everything/test" + component: "rhine-koans:test:basic-2-6-count-everything-test" + + - path: "./Main.hs" + component: "rhine-koans:exe:basic-2-7-count-everything-nicer" + + - path: "koans/basic/2/7-count-everything-nicer/test" + component: "rhine-koans:test:basic-2-7-count-everything-nicer-test" + + - path: "./Main.hs" + component: "rhine-koans:exe:basic-2-8-dont-count-everything" + + - path: "koans/basic/2/8-dont-count-everything/test" + component: "rhine-koans:test:basic-2-8-dont-count-everything-test" + + - path: "./Main.hs" + component: "rhine-koans:exe:basic-2-9-modularize" + + - path: "koans/basic/2/9-modularize/test" + component: "rhine-koans:test:basic-2-9-modularize-test" + + - path: "generic/reimport-main/Main.hs" + component: "rhine-koans:exe:ui-1-gloss-1-circle" + + - path: "generic/reimport-main/Main.hs" + component: "rhine-koans:test:ui-1-gloss-1-circle-test" + + - path: "koans/ui/1-gloss/1-circle/test" + component: "rhine-koans:test:ui-1-gloss-1-circle-test" + + - path: "koans/ui/1-gloss/1-circle" + component: "rhine-koans:test:ui-1-gloss-1-circle-test" + + - path: "koans/ui/1-gloss/1-circle/solution" + component: "rhine-koans:test:ui-1-gloss-1-circle-test" + + - path: "koans/ui/1-gloss/1-circle" + component: "rhine-koans:exe:ui-1-gloss-1-circle" + + - path: "koans/ui/1-gloss/1-circle/solution" + component: "rhine-koans:exe:ui-1-gloss-1-circle" + + - path: "generic/reimport-main/Main.hs" + component: "rhine-koans:exe:ui-1-gloss-2-move" + + - path: "generic/reimport-main/Main.hs" + component: "rhine-koans:test:ui-1-gloss-2-move-test" + + - path: "koans/ui/1-gloss/2-move/test" + component: "rhine-koans:test:ui-1-gloss-2-move-test" + + - path: "koans/ui/1-gloss/2-move" + component: "rhine-koans:test:ui-1-gloss-2-move-test" + + - path: "koans/ui/1-gloss/2-move/solution" + component: "rhine-koans:test:ui-1-gloss-2-move-test" + + - path: "koans/ui/1-gloss/2-move" + component: "rhine-koans:exe:ui-1-gloss-2-move" + + - path: "koans/ui/1-gloss/2-move/solution" + component: "rhine-koans:exe:ui-1-gloss-2-move" + + - path: "generic/reimport-main/Main.hs" + component: "rhine-koans:exe:ui-1-gloss-3-modularize" + + - path: "generic/reimport-main/Main.hs" + component: "rhine-koans:test:ui-1-gloss-3-modularize-test" + + - path: "koans/ui/1-gloss/3-modularize/test" + component: "rhine-koans:test:ui-1-gloss-3-modularize-test" + + - path: "koans/ui/1-gloss/3-modularize" + component: "rhine-koans:test:ui-1-gloss-3-modularize-test" + + - path: "koans/ui/1-gloss/3-modularize/solution" + component: "rhine-koans:test:ui-1-gloss-3-modularize-test" + + - path: "koans/ui/1-gloss/3-modularize" + component: "rhine-koans:exe:ui-1-gloss-3-modularize" + + - path: "koans/ui/1-gloss/3-modularize/solution" + component: "rhine-koans:exe:ui-1-gloss-3-modularize" + + - path: "generic/reimport-main/Main.hs" + component: "rhine-koans:exe:ui-1-gloss-4-user-input" + + - path: "generic/reimport-main/Main.hs" + component: "rhine-koans:test:ui-1-gloss-4-user-input-test" + + - path: "koans/ui/1-gloss/4-user-input/test" + component: "rhine-koans:test:ui-1-gloss-4-user-input-test" + + - path: "koans/ui/1-gloss/4-user-input" + component: "rhine-koans:test:ui-1-gloss-4-user-input-test" + + - path: "koans/ui/1-gloss/4-user-input/solution" + component: "rhine-koans:test:ui-1-gloss-4-user-input-test" + + - path: "koans/ui/1-gloss/4-user-input" + component: "rhine-koans:exe:ui-1-gloss-4-user-input" + + - path: "koans/ui/1-gloss/4-user-input/solution" + component: "rhine-koans:exe:ui-1-gloss-4-user-input" + + - path: "generic/reimport-main/Main.hs" + component: "rhine-koans:exe:ui-1-gloss-5-randomness" + + - path: "generic/reimport-main/Main.hs" + component: "rhine-koans:test:ui-1-gloss-5-randomness-test" + + - path: "koans/ui/1-gloss/5-randomness/test" + component: "rhine-koans:test:ui-1-gloss-5-randomness-test" + + - path: "koans/ui/1-gloss/5-randomness" + component: "rhine-koans:test:ui-1-gloss-5-randomness-test" + + - path: "koans/ui/1-gloss/5-randomness/solution" + component: "rhine-koans:test:ui-1-gloss-5-randomness-test" + + - path: "koans/ui/1-gloss/5-randomness" + component: "rhine-koans:exe:ui-1-gloss-5-randomness" + + - path: "koans/ui/1-gloss/5-randomness/solution" + component: "rhine-koans:exe:ui-1-gloss-5-randomness" + + - path: "generic/reimport-main/Main.hs" + component: "rhine-koans:exe:ui-1-gloss-6-control-flow" + + - path: "generic/reimport-main/Main.hs" + component: "rhine-koans:test:ui-1-gloss-6-control-flow-test" + + - path: "koans/ui/1-gloss/6-control-flow/test" + component: "rhine-koans:test:ui-1-gloss-6-control-flow-test" + + - path: "koans/ui/1-gloss/6-control-flow" + component: "rhine-koans:test:ui-1-gloss-6-control-flow-test" + + - path: "koans/ui/1-gloss/6-control-flow/solution" + component: "rhine-koans:test:ui-1-gloss-6-control-flow-test" + + - path: "koans/ui/1-gloss/6-control-flow" + component: "rhine-koans:exe:ui-1-gloss-6-control-flow" + + - path: "koans/ui/1-gloss/6-control-flow/solution" + component: "rhine-koans:exe:ui-1-gloss-6-control-flow" + + - path: "generic/reimport-main/Main.hs" + component: "rhine-koans:exe:ui-1-gloss-snake" + + - path: "generic/reimport-main/Main.hs" + component: "rhine-koans:test:ui-1-gloss-snake-test" + + - path: "koans/ui/1-gloss/snake/test" + component: "rhine-koans:test:ui-1-gloss-snake-test" + + - path: "koans/ui/1-gloss/snake" + component: "rhine-koans:test:ui-1-gloss-snake-test" + + - path: "koans/ui/1-gloss/snake/solution" + component: "rhine-koans:test:ui-1-gloss-snake-test" + + - path: "koans/ui/1-gloss/snake" + component: "rhine-koans:exe:ui-1-gloss-snake" + + - path: "koans/ui/1-gloss/snake/solution" + component: "rhine-koans:exe:ui-1-gloss-snake" + + - path: "koans/wsml" + component: "rhine-koans:exe:wsml" diff --git a/koans/ui/1-gloss/1-circle/Koan.hs b/koans/ui/1-gloss/1-circle/Koan.hs new file mode 100644 index 0000000..44c20a4 --- /dev/null +++ b/koans/ui/1-gloss/1-circle/Koan.hs @@ -0,0 +1,41 @@ +{- | Circle. + +Let's draw something! +Rhine connects to the famous gloss library for 2d graphics. +Have a look at https://hackage.haskell.org/package/gloss to learn more about it! + +The connection between Rhine and gloss is provided by the library https://hackage.haskell.org/package/rhine-gloss, +which encapsulates the effects of drawing pictures in gloss in a monad, 'GlossConcT', +and provides several clocks to interact with the gloss system. + +To warm up, let's just draw a circle. +-} +module Koan where + +-- rhine +import FRP.Rhine + +-- rhine-gloss +import FRP.Rhine.Gloss + +{- | The main 'Rhine' of this program. + + /--- We use effects in 'GlossConcT' to draw images. + | + | /--- This clock ticks whenever an image is drawn on the screen by the gloss backend. + | | + v v +-} +rhine :: Rhine (GlossConcT IO) GlossSimClockIO () () +-- Can you create a solid circle of radius 10 here? +-- Have a look at https://hackage.haskell.org/package/gloss/docs/Graphics-Gloss-Data-Picture.html for inspiration. +rhine = + constMCl (paintAllIO _) -- paintAllIO clears the drawing canvas and draws the given image + @@ GlossSimClockIO -- The singleton value of GlossSimClockIO. + +main :: IO () +-- Make sure to keep this definition here as it is: The tests depend on it. +main = + flowGlossIO -- This function can replace 'flow' when you're using the gloss backend. + defaultSettings -- Settings for the gloss window context such as size, title, and background colour. + rhine diff --git a/koans/ui/1-gloss/1-circle/solution/Koan.hs b/koans/ui/1-gloss/1-circle/solution/Koan.hs new file mode 100644 index 0000000..d247959 --- /dev/null +++ b/koans/ui/1-gloss/1-circle/solution/Koan.hs @@ -0,0 +1,41 @@ +{- | Circle. + +Let's draw something! +Rhine connects to the famous gloss library for 2d graphics. +Have a look at https://hackage.haskell.org/package/gloss to learn more about it! + +The connection between Rhine and gloss is provided by the library https://hackage.haskell.org/package/rhine-gloss, +which encapsulates the effects of drawing pictures in gloss in a monad, 'GlossConcT', +and provides several clocks to interact with the gloss system. + +To warm up, let's just draw a circle. +-} +module Koan where + +-- rhine +import FRP.Rhine + +-- rhine-gloss +import FRP.Rhine.Gloss + +{- | The main 'Rhine' of this program. + + /--- We use effects in 'GlossConcT' to draw images. + | + | /--- This clock ticks whenever an image is drawn on the screen by the gloss backend. + | | + v v +-} +rhine :: Rhine (GlossConcT IO) GlossSimClockIO () () +-- Can you create a solid circle of radius 10 here? +-- Have a look at https://hackage.haskell.org/package/gloss/docs/Graphics-Gloss-Data-Picture.html for inspiration. +rhine = + constMCl (paintAllIO (circleSolid 10)) -- paintAllIO clears the drawing canvas and draws the given image + @@ GlossSimClockIO -- The singleton value of GlossSimClockIO. + +main :: IO () +-- Make sure to keep this definition here as it is: The tests depend on it. +main = + flowGlossIO -- This function can replace 'flow' when you're using the gloss backend. + defaultSettings -- Settings for the gloss window context such as size, title, and background colour. + rhine diff --git a/koans/ui/1-gloss/1-circle/test/Test.hs b/koans/ui/1-gloss/1-circle/test/Test.hs new file mode 100644 index 0000000..1f03e97 --- /dev/null +++ b/koans/ui/1-gloss/1-circle/test/Test.hs @@ -0,0 +1,15 @@ +module Main where + +-- rhine-gloss +import FRP.Rhine.Gloss + +-- test-gloss +import TestGloss + +-- koan +import Koan (rhine) + +main :: IO () +main = do + [pic] <- stepGlossRhine rhine [1] + expectPic pic [circleSolid 10] diff --git a/koans/ui/1-gloss/2-move/Koan.hs b/koans/ui/1-gloss/2-move/Koan.hs new file mode 100644 index 0000000..376f74a --- /dev/null +++ b/koans/ui/1-gloss/2-move/Koan.hs @@ -0,0 +1,27 @@ +{- | Move. + +One central idea of Functional Reactive Animation (Conal Elliot & Paul Hudak, ICFP 1997) +is that an animation is a picture parametrised by time. +This idea is continued in Yampa and Rhine by providing knowledge of time as a builtin effect, +which can then be used to parametrise everything the program does. + +In Rhine, one way to access time is to use ['sinceInitS'](https://hackage.haskell.org/package/rhine/docs/FRP-Rhine-ClSF-Util.html#v:sinceInitS), +which outputs the time since clock initialisation (which happens at the beginning of 'flow'). +When you use this time to translate the position of the circle, it will move! +-} +module Koan where + +-- rhine +import FRP.Rhine + +-- rhine-gloss +import FRP.Rhine.Gloss + +-- | The main 'Rhine' of this program. +rhine :: Rhine (GlossConcT IO) GlossSimClockIO () () +-- Somehow the order of these functions is wrong. Can you fix it? +rhine = sinceInitS >-> arrMCl (\t -> translate 0 (10 * t) $ paintAllIO $ circleSolid 10) @@ GlossSimClockIO + +main :: IO () +-- Make sure to keep this definition here as it is: The tests depend on it. +main = flowGlossIO defaultSettings rhine diff --git a/koans/ui/1-gloss/2-move/solution/Koan.hs b/koans/ui/1-gloss/2-move/solution/Koan.hs new file mode 100644 index 0000000..9172db7 --- /dev/null +++ b/koans/ui/1-gloss/2-move/solution/Koan.hs @@ -0,0 +1,26 @@ +{- | Move. + +One central idea of Functional Reactive Animation (Conal Elliot & Paul Hudak, ICFP 1997) +is that an animation is a picture parametrised by time. +This idea is continued in Yampa and Rhine by providing knowledge of time as a builtin effect, +which can then be used to parametrise everything the program does. + +In Rhine, one way to access time is to use ['sinceInitS'](https://hackage.haskell.org/package/rhine/docs/FRP-Rhine-ClSF-Util.html#v:sinceInitS), +which outputs the time since clock initialisation (which happens at the beginning of 'flow'). +When you use this time to translate the position of the circle, it will move! +-} +module Koan where + +-- rhine +import FRP.Rhine + +-- rhine-gloss +import FRP.Rhine.Gloss + +-- | The main 'Rhine' of this program. +rhine :: Rhine (GlossConcT IO) GlossSimClockIO () () +rhine = sinceInitS >-> arrMCl (\t -> paintAllIO $ translate 0 (10 * t) $ circleSolid 10) @@ GlossSimClockIO + +main :: IO () +-- Make sure to keep this definition here as it is: The tests depend on it. +main = flowGlossIO defaultSettings rhine diff --git a/koans/ui/1-gloss/2-move/test/Test.hs b/koans/ui/1-gloss/2-move/test/Test.hs new file mode 100644 index 0000000..c7c95ac --- /dev/null +++ b/koans/ui/1-gloss/2-move/test/Test.hs @@ -0,0 +1,16 @@ +module Main where + +-- rhine-gloss +import FRP.Rhine.Gloss + +-- test-gloss +import TestGloss + +-- koan +import Koan (rhine) + +main :: IO () +main = do + [pic1, pic2] <- stepGlossRhine rhine [0, 1] + expectPic pic1 [translate 0 0 $ circleSolid 10] + expectPic pic2 [translate 0 10 $ circleSolid 10] diff --git a/koans/ui/1-gloss/3-modularize/Koan.hs b/koans/ui/1-gloss/3-modularize/Koan.hs new file mode 100644 index 0000000..0c7da2c --- /dev/null +++ b/koans/ui/1-gloss/3-modularize/Koan.hs @@ -0,0 +1,66 @@ +{- | Modularize. + +To make a round-based game, we need to encode the rounds in some way. +The most natural way to do this in Rhine is to define a separate clock where each tick corresponds to one round! + +Let's do this here. +For the rest of this track, we will just assume that a round lasts half a second. +So we should use a @'Millisecond' 500@ clock! + +The devil is in the details, though. +We now have two different components, the game clock and the visualization clock. +But they run on different monads and time domains! +You will have to translate between them in order to make everything flow together. +-} +module Koan where + +-- rhine +import FRP.Rhine + +-- rhine-gloss +import FRP.Rhine.Gloss + +-- * Game logic + +{- | A circle that moves upwards by 10 pixels every second. + +Its type signature ensures that it will be run on the 'GameClock'. +-} +movingCircle :: ClSF GlossConc GameClock () Picture +-- The cryptic type error wants to tell us that the time since clock initialisation is in Double, but gloss expects a Float! +-- Can you convert one to the other? +movingCircle = sinceInitS >-> arr (\t -> translate 0 (10 * t) $ circleSolid 10) -- realToFrac works as well! + +-- | A clock that ticks at every round of the game. +type GameClock = + -- Actually we just want a Millisecond 500 clock, but that is in the 'IO' monad, + -- while the gloss backend expects a particular monad, 'GlossConcT'. + -- Luckily there is also has a utility to lift any 'IO' clock to it! + -- Have a look at https://hackage.haskell.org/package/rhine-gloss/docs/FRP-Rhine-Gloss-IO.html. + _ _ (Millisecond 500) + +gameClock :: GameClock +-- The clock type lifting function from above also has a corresponding value level function! +gameClock = _ waitClock + +-- * Visualization + +-- | Paint a gloss picture +visualize :: BehaviourF GlossConc UTCTime Picture () +visualize = arrMCl paintAllIO + +-- | Draw at 30 FPS +type VisualizationClock = _ _ GlossSimClockIO + +visualizationClock :: VisualizationClock +visualizationClock = _ GlossSimClockIO + +-- Find the right resampling buffer to transport the rendered image from the game clock to the visualization clock. +-- It should have two properties: +-- 1. It should always output the newest image. +-- 2. At startup, before the first round of the game has started, a blank image should be displayed. +rhine = movingCircle @@ gameClock >-- _ blank --> visualize @@ visualizationClock + +main :: IO () +-- Make sure to keep this definition here as it is: The tests depend on it. +main = flowGlossIO defaultSettings rhine diff --git a/koans/ui/1-gloss/3-modularize/solution/Koan.hs b/koans/ui/1-gloss/3-modularize/solution/Koan.hs new file mode 100644 index 0000000..f91040b --- /dev/null +++ b/koans/ui/1-gloss/3-modularize/solution/Koan.hs @@ -0,0 +1,69 @@ +{- | Modularize. + +To make a round-based game, we need to encode the rounds in some way. +The most natural way to do this in Rhine is to define a separate clock where each tick corresponds to one round! + +Let's do this here. +For the rest of this track, we will just assume that a round lasts half a second. +So we should use a @'Millisecond' 500@ clock! + +The devil is in the details, though. +We now have two different components, the game clock and the visualization clock. +But they run on different monads and time domains! +You will have to translate between them in order to make everything flow together. +-} +module Koan where + +-- base +import GHC.Float (double2Float) + +-- rhine +import FRP.Rhine + +-- rhine-gloss +import FRP.Rhine.Gloss + +-- * Game logic + +{- | A circle that moves upwards by 10 pixels every second. + +Its type signature ensures that it will be run on the 'GameClock'. +-} +movingCircle :: ClSF GlossConc GameClock () Picture +-- The cryptic type error wants to tell us that the time since clock initialisation is in Double, but gloss expects a Float! +-- Can you convert one to the other? +movingCircle = sinceInitS >-> arr (\t -> translate 0 (10 * double2Float t) $ circleSolid 10) -- realToFrac works as well! + +-- | A clock that ticks at every round of the game. +type GameClock = + -- Actually we just want a Millisecond 500 clock, but that is in the 'IO' monad, + -- while the gloss backend expects a particular monad, 'GlossConcT'. + -- Luckily there is also has a utility to lift any 'IO' clock to it! + -- Have a look at https://hackage.haskell.org/package/rhine-gloss/docs/FRP-Rhine-Gloss-IO.html. + GlossConcTClock IO (Millisecond 500) + +gameClock :: GameClock +-- The clock type lifting function from above also has a corresponding value level function! +gameClock = glossConcTClock waitClock + +-- * Visualization + +-- | Paint a gloss picture +visualize :: BehaviourF GlossConc UTCTime Picture () +visualize = arrMCl paintAllIO + +-- | Draw at 30 FPS +type VisualizationClock = GlossClockUTC IO GlossSimClockIO + +visualizationClock :: VisualizationClock +visualizationClock = glossClockUTC GlossSimClockIO + +-- Find the right resampling buffer to transport the rendered image from the game clock to the visualization clock. +-- It should have two properties: +-- 1. It should always output the newest image. +-- 2. At startup, before the first round of the game has started, a blank image should be displayed. +rhine = movingCircle @@ gameClock >-- keepLast blank --> visualize @@ visualizationClock + +main :: IO () +-- Make sure to keep this definition here as it is: The tests depend on it. +main = flowGlossIO defaultSettings rhine diff --git a/koans/ui/1-gloss/3-modularize/test/Test.hs b/koans/ui/1-gloss/3-modularize/test/Test.hs new file mode 100644 index 0000000..a09fa81 --- /dev/null +++ b/koans/ui/1-gloss/3-modularize/test/Test.hs @@ -0,0 +1,38 @@ +module Main where + +-- base +import Control.Concurrent +import Control.Monad +import Data.IORef +import System.Exit + +-- transformers +import Control.Monad.Trans.Reader + +-- monad-schedule +import Control.Monad.Schedule.FreeAsync (runFreeAsyncT) + +-- rhine-gloss +import FRP.Rhine.Gloss + +-- koan +import Koan (rhine) + +flattenPictures :: Picture -> [Picture] +flattenPictures (Pictures ps) = ps >>= flattenPictures +flattenPictures Blank = [] +flattenPictures picture = [picture] + +main :: IO () +main = do + vars <- liftIO $ GlossEnv <$> newEmptyMVar <*> newEmptyMVar <*> newIORef Blank <*> newIORef 0 + void $ forkIO $ runFreeAsyncT $ runReaderT (unGlossConcT (flow rhine)) vars + putMVar (timeVar vars) 1 + putMVar (timeVar vars) 2 + threadDelay 100000 + pic <- readIORef (picRef vars) + when (flattenPictures pic /= [rectangleSolid 1 1]) $ do + print [rectangleSolid 1 1] + print pic + print $ flattenPictures pic + exitFailure diff --git a/koans/ui/1-gloss/4-user-input/Koan.hs b/koans/ui/1-gloss/4-user-input/Koan.hs new file mode 100644 index 0000000..ec0e85c --- /dev/null +++ b/koans/ui/1-gloss/4-user-input/Koan.hs @@ -0,0 +1,221 @@ +{-# LANGUAGE Arrows #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE TemplateHaskell #-} + +module Koan where + +-- base +import Data.List.NonEmpty hiding (insert, unfold) +import Data.Maybe (fromMaybe) +import GHC.Generics + +-- random +import System.Random + +-- MonadRandom +import Control.Monad.Random + +-- containers +import Data.Set hiding (toList) + +-- rhine +import FRP.Rhine + +-- rhine-gloss +import FRP.Rhine.Gloss +import System.Random.Stateful (UniformRange (..)) +import Prelude hiding (head) + +-- * Grid positions on the playing board + +boardSize :: Int +boardSize = 20 + +data Position = Position + { x :: Int + , y :: Int + } + deriving (Generic, Eq, Ord) + +instance Semigroup Position where + Position x1 y1 <> Position x2 y2 = Position (x1 + x2) (y1 + y2) + +instance Monoid Position where + mempty = Position 0 0 + +-- | To generate random apple positions +instance Uniform Position + +instance UniformRange Position where + uniformRM (Position xLow yLow, Position xHigh yHigh) g = Position <$> uniformRM (xLow, xHigh) g <*> uniformRM (yLow, yHigh) g + +instance Random Position + +renderPosition :: Position -> Picture +renderPosition Position {x, y} = translate (fromIntegral x) (fromIntegral y) $ circleSolid 0.6 + +-- * Directions in which the snake can head + +data Direction = East | North | West | South + deriving (Enum) + +-- | A position changes by a direction in one step +stepPosition :: Direction -> Position -> Position +stepPosition East = (<> Position 1 0) +stepPosition North = (<> Position 0 1) +stepPosition West = (<> Position (-1) 0) +stepPosition South = (<> Position 0 (-1)) + +-- | The user can change the direction of the snake +data Turn + = -- | Don't change the direction. This happens if no key is pressed. + Stay + | -- | Turn right (clockwise) when the right arrow is pressed. + TurnRight + | -- | Turn left (counterclockwise) when the left arrow is pressed. + TurnLeft + deriving (Show) + +changeDirection :: Turn -> Direction -> Direction +changeDirection Stay direction = direction +changeDirection TurnRight direction = toEnum $ (fromEnum direction - 1) `mod` 4 +changeDirection TurnLeft direction = toEnum $ (fromEnum direction + 1) `mod` 4 + +-- | Whether the snake currently eats an apple. +data Eat = Eat | DontEat + +data Snake = Snake + { direction :: Direction + , body :: NonEmpty Position + } + +-- | A small snake. +snek :: Direction -> Position -> Snake +snek direction tinyBody = + Snake + { direction + , body = pure tinyBody + } + +-- | On every step, a snake can make a turn, and possibly eat an apple +stepSnake :: Turn -> Eat -> Snake -> Snake +stepSnake turn eat snake = + let + newDirection = changeDirection turn $ direction snake + newHead = stepPosition newDirection $ Data.List.NonEmpty.head $ body snake + newTail = tailAfterMeal eat snake + in + Snake + { direction = newDirection + , body = newHead :| newTail + } + where + tailAfterMeal :: Eat -> Snake -> [Position] + tailAfterMeal DontEat = Data.List.NonEmpty.init . body + tailAfterMeal Eat = toList . body + +renderSnake :: Snake -> Picture +renderSnake = foldMap renderPosition . body + +newtype Apple = Apple {getApple :: Position} + deriving (Eq, Ord) + +newApple :: (MonadRandom m) => ClSF m GameClock () (Maybe Apple) +newApple = proc _ -> do + nSteps :: Int <- count -< () + if nSteps `mod` 10 == 0 + then arr (Just <<< Apple) <<< getRandomRS -< (Position (-10) (-10), Position 10 10) + else returnA -< Nothing + +type Apples = Set Apple + +addAndEatApple :: + -- | Possibly a new apple appeared + Maybe Apple -> + -- | On this position the snake attempted to eat the apple + Position -> + -- | The previous collection of apples + Apples -> + (Apples, Eat) +addAndEatApple addedApple eatPosition oldApples = + let addedApples = maybe oldApples (`insert` oldApples) addedApple + newApples = delete (Apple eatPosition) addedApples + in (newApples, if size newApples < size addedApples then Eat else DontEat) + +renderApple :: Apple -> Picture +renderApple = color red . renderPosition . getApple + +type GameClock = GlossConcTClock IO (Millisecond 500) + +gameClock :: GameClock +gameClock = glossConcTClock waitClock + +snakeSF :: ClSF GlossConc GameClock (Turn, Eat) Snake +snakeSF = unfold (snek North mempty) $ \(turn, eat) s -> let s' = stepSnake turn eat s in Result s' s' + +applesSF :: ClSF GlossConc GameClock Position (Apples, Eat) +applesSF = feedback empty $ proc (eatPosition, oldApples) -> do + addedApple <- evalRandIOS' newApple -< () + let (newApples, eat) = addAndEatApple addedApple eatPosition oldApples + returnA -< ((newApples, eat), newApples) + +snakeAndApples :: ClSF GlossConc GameClock Turn (Snake, Apples) +snakeAndApples = feedback DontEat $ proc (turn, eat) -> do + snake <- snakeSF -< (turn, eat) + (apples, eatNext) <- applesSF -< head $ body snake + returnA -< ((snake, apples), eatNext) + +-- | Whether a snake hits the boundaries or bites itself +illegal :: Snake -> Bool +illegal Snake {body = head@Position {x, y} :| tail} = + head `elem` tail + || x < (-boardSize) + || x > boardSize + || y < (-boardSize) + || y > boardSize + +game :: ClSF GlossConc GameClock Turn (Maybe (Snake, Apples)) +game = safely $ do + try $ liftClSF snakeAndApples >>> throwOnCond (fst >>> illegal) () >>> arr Just + safe $ pure Nothing + +render :: Maybe (Snake, Apples) -> Picture +render (Just (snake, apples)) = renderSnake snake <> foldMap renderApple apples +render Nothing = gameover + +gameover :: Picture +gameover = translate (-10) 0 $ scale 0.03 0.03 $ text "Game over!" + +-- | Scale and paint a gloss picture +visualize :: BehaviourF GlossConc UTCTime Picture () +visualize = arrMCl $ scale 10 10 >>> paintAllIO + +-- | Draw at 30 FPS +type VisualizationClock = GlossClockUTC IO GlossSimClockIO + +visualizationClock :: VisualizationClock +visualizationClock = glossClockUTC GlossSimClockIO + +-- | Select only those input events that correspond to turns of the snake +type UserClock = GlossClockUTC IO (SelectClock GlossEventClockIO Turn) + +userClock :: UserClock +userClock = + glossClockUTC $ + SelectClock + { mainClock = GlossEventClockIO + , select = \case + (EventKey (SpecialKey KeyRight) Down _ _) -> Just TurnRight + (EventKey (SpecialKey KeyLeft) Down _ _) -> Just TurnLeft + _ -> Nothing + } + +-- | User input to turn the snake +user :: ClSF GlossConc UserClock () Turn +user = tagS + +rhine = user @@ userClock >-- fifoBounded 1000 --> (arr (fromMaybe Stay) >-> game >-> arr render @@ gameClock) >-- keepLast mempty --> visualize @@ visualizationClock + +main :: IO () +-- Make sure to keep this definition here as it is: The tests depend on it. +main = flowGlossIO defaultSettings rhine diff --git a/koans/ui/1-gloss/4-user-input/solution/Koan.hs b/koans/ui/1-gloss/4-user-input/solution/Koan.hs new file mode 100644 index 0000000..e517640 --- /dev/null +++ b/koans/ui/1-gloss/4-user-input/solution/Koan.hs @@ -0,0 +1,109 @@ +{-# LANGUAGE Arrows #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE TemplateHaskell #-} + +-- | Grid. +module Koan where + +-- base +import Data.List.NonEmpty hiding (insert, unfold) +import Data.Maybe (fromMaybe) +import GHC.Generics + +-- rhine +import FRP.Rhine + +-- rhine-gloss +import FRP.Rhine.Gloss + +-- * Grid positions on the playing board + +boardSize :: Int +boardSize = 20 + +data Position = Position + { x :: Int + , y :: Int + } + deriving (Generic, Eq, Ord) + +instance Semigroup Position where + Position x1 y1 <> Position x2 y2 = Position (x1 + x2) (y1 + y2) + +instance Monoid Position where + mempty = Position 0 0 + +renderPosition :: Position -> Picture +renderPosition Position {x, y} = translate (fromIntegral x) (fromIntegral y) $ circleSolid 0.6 + +-- * Directions in which the snake can head + +data Direction = East | North | West | South + deriving (Enum) + +-- | A position changes by a direction in one step +stepPosition :: Direction -> Position -> Position +stepPosition East = (<> Position 1 0) +stepPosition North = (<> Position 0 1) +stepPosition West = (<> Position (-1) 0) +stepPosition South = (<> Position 0 (-1)) + +-- | The user can change the direction of the snake +data Turn + = -- | Don't change the direction. This happens if no key is pressed. + Stay + | -- | Turn right (clockwise) when the right arrow is pressed. + TurnRight + | -- | Turn left (counterclockwise) when the left arrow is pressed. + TurnLeft + deriving (Show) + +changeDirection :: Turn -> Direction -> Direction +changeDirection Stay direction = direction +changeDirection TurnRight direction = toEnum $ (fromEnum direction - 1) `mod` 4 +changeDirection TurnLeft direction = toEnum $ (fromEnum direction + 1) `mod` 4 + +type GameClock = GlossConcTClock IO (Millisecond 500) + +gameClock :: GameClock +gameClock = glossConcTClock waitClock + +game :: ClSF GlossConc GameClock Turn Position +game = unfold (mempty, North) $ \turn (position, direction) -> + let direction' = changeDirection turn direction + position' = stepPosition direction' position + in Result (position', direction') position' + +-- | Scale and paint a gloss picture +visualize :: BehaviourF GlossConc UTCTime Picture () +visualize = arrMCl $ scale 10 10 >>> paintAllIO + +-- | Draw at 30 FPS +type VisualizationClock = GlossClockUTC IO GlossSimClockIO + +visualizationClock :: VisualizationClock +visualizationClock = glossClockUTC GlossSimClockIO + +-- | Select only those input events that correspond to turns of the snake +type UserClock = GlossClockUTC IO (SelectClock GlossEventClockIO Turn) + +userClock :: UserClock +userClock = + glossClockUTC $ + SelectClock + { mainClock = GlossEventClockIO + , select = \case + (EventKey (SpecialKey KeyRight) Down _ _) -> Just TurnRight + (EventKey (SpecialKey KeyLeft) Down _ _) -> Just TurnLeft + _ -> Nothing + } + +-- | User input to turn the snake +user :: ClSF GlossConc UserClock () Turn +user = tagS + +rhine = user @@ userClock >-- fifoBounded 1000 --> (arr (fromMaybe Stay) >-> game >-> arr renderPosition @@ gameClock) >-- keepLast mempty --> visualize @@ visualizationClock + +main :: IO () +-- Make sure to keep this definition here as it is: The tests depend on it. +main = flowGlossIO defaultSettings rhine diff --git a/koans/ui/1-gloss/4-user-input/test/Test.hs b/koans/ui/1-gloss/4-user-input/test/Test.hs new file mode 100644 index 0000000..a09fa81 --- /dev/null +++ b/koans/ui/1-gloss/4-user-input/test/Test.hs @@ -0,0 +1,38 @@ +module Main where + +-- base +import Control.Concurrent +import Control.Monad +import Data.IORef +import System.Exit + +-- transformers +import Control.Monad.Trans.Reader + +-- monad-schedule +import Control.Monad.Schedule.FreeAsync (runFreeAsyncT) + +-- rhine-gloss +import FRP.Rhine.Gloss + +-- koan +import Koan (rhine) + +flattenPictures :: Picture -> [Picture] +flattenPictures (Pictures ps) = ps >>= flattenPictures +flattenPictures Blank = [] +flattenPictures picture = [picture] + +main :: IO () +main = do + vars <- liftIO $ GlossEnv <$> newEmptyMVar <*> newEmptyMVar <*> newIORef Blank <*> newIORef 0 + void $ forkIO $ runFreeAsyncT $ runReaderT (unGlossConcT (flow rhine)) vars + putMVar (timeVar vars) 1 + putMVar (timeVar vars) 2 + threadDelay 100000 + pic <- readIORef (picRef vars) + when (flattenPictures pic /= [rectangleSolid 1 1]) $ do + print [rectangleSolid 1 1] + print pic + print $ flattenPictures pic + exitFailure diff --git a/koans/ui/1-gloss/5-randomness/Koan.hs b/koans/ui/1-gloss/5-randomness/Koan.hs new file mode 100644 index 0000000..ec0e85c --- /dev/null +++ b/koans/ui/1-gloss/5-randomness/Koan.hs @@ -0,0 +1,221 @@ +{-# LANGUAGE Arrows #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE TemplateHaskell #-} + +module Koan where + +-- base +import Data.List.NonEmpty hiding (insert, unfold) +import Data.Maybe (fromMaybe) +import GHC.Generics + +-- random +import System.Random + +-- MonadRandom +import Control.Monad.Random + +-- containers +import Data.Set hiding (toList) + +-- rhine +import FRP.Rhine + +-- rhine-gloss +import FRP.Rhine.Gloss +import System.Random.Stateful (UniformRange (..)) +import Prelude hiding (head) + +-- * Grid positions on the playing board + +boardSize :: Int +boardSize = 20 + +data Position = Position + { x :: Int + , y :: Int + } + deriving (Generic, Eq, Ord) + +instance Semigroup Position where + Position x1 y1 <> Position x2 y2 = Position (x1 + x2) (y1 + y2) + +instance Monoid Position where + mempty = Position 0 0 + +-- | To generate random apple positions +instance Uniform Position + +instance UniformRange Position where + uniformRM (Position xLow yLow, Position xHigh yHigh) g = Position <$> uniformRM (xLow, xHigh) g <*> uniformRM (yLow, yHigh) g + +instance Random Position + +renderPosition :: Position -> Picture +renderPosition Position {x, y} = translate (fromIntegral x) (fromIntegral y) $ circleSolid 0.6 + +-- * Directions in which the snake can head + +data Direction = East | North | West | South + deriving (Enum) + +-- | A position changes by a direction in one step +stepPosition :: Direction -> Position -> Position +stepPosition East = (<> Position 1 0) +stepPosition North = (<> Position 0 1) +stepPosition West = (<> Position (-1) 0) +stepPosition South = (<> Position 0 (-1)) + +-- | The user can change the direction of the snake +data Turn + = -- | Don't change the direction. This happens if no key is pressed. + Stay + | -- | Turn right (clockwise) when the right arrow is pressed. + TurnRight + | -- | Turn left (counterclockwise) when the left arrow is pressed. + TurnLeft + deriving (Show) + +changeDirection :: Turn -> Direction -> Direction +changeDirection Stay direction = direction +changeDirection TurnRight direction = toEnum $ (fromEnum direction - 1) `mod` 4 +changeDirection TurnLeft direction = toEnum $ (fromEnum direction + 1) `mod` 4 + +-- | Whether the snake currently eats an apple. +data Eat = Eat | DontEat + +data Snake = Snake + { direction :: Direction + , body :: NonEmpty Position + } + +-- | A small snake. +snek :: Direction -> Position -> Snake +snek direction tinyBody = + Snake + { direction + , body = pure tinyBody + } + +-- | On every step, a snake can make a turn, and possibly eat an apple +stepSnake :: Turn -> Eat -> Snake -> Snake +stepSnake turn eat snake = + let + newDirection = changeDirection turn $ direction snake + newHead = stepPosition newDirection $ Data.List.NonEmpty.head $ body snake + newTail = tailAfterMeal eat snake + in + Snake + { direction = newDirection + , body = newHead :| newTail + } + where + tailAfterMeal :: Eat -> Snake -> [Position] + tailAfterMeal DontEat = Data.List.NonEmpty.init . body + tailAfterMeal Eat = toList . body + +renderSnake :: Snake -> Picture +renderSnake = foldMap renderPosition . body + +newtype Apple = Apple {getApple :: Position} + deriving (Eq, Ord) + +newApple :: (MonadRandom m) => ClSF m GameClock () (Maybe Apple) +newApple = proc _ -> do + nSteps :: Int <- count -< () + if nSteps `mod` 10 == 0 + then arr (Just <<< Apple) <<< getRandomRS -< (Position (-10) (-10), Position 10 10) + else returnA -< Nothing + +type Apples = Set Apple + +addAndEatApple :: + -- | Possibly a new apple appeared + Maybe Apple -> + -- | On this position the snake attempted to eat the apple + Position -> + -- | The previous collection of apples + Apples -> + (Apples, Eat) +addAndEatApple addedApple eatPosition oldApples = + let addedApples = maybe oldApples (`insert` oldApples) addedApple + newApples = delete (Apple eatPosition) addedApples + in (newApples, if size newApples < size addedApples then Eat else DontEat) + +renderApple :: Apple -> Picture +renderApple = color red . renderPosition . getApple + +type GameClock = GlossConcTClock IO (Millisecond 500) + +gameClock :: GameClock +gameClock = glossConcTClock waitClock + +snakeSF :: ClSF GlossConc GameClock (Turn, Eat) Snake +snakeSF = unfold (snek North mempty) $ \(turn, eat) s -> let s' = stepSnake turn eat s in Result s' s' + +applesSF :: ClSF GlossConc GameClock Position (Apples, Eat) +applesSF = feedback empty $ proc (eatPosition, oldApples) -> do + addedApple <- evalRandIOS' newApple -< () + let (newApples, eat) = addAndEatApple addedApple eatPosition oldApples + returnA -< ((newApples, eat), newApples) + +snakeAndApples :: ClSF GlossConc GameClock Turn (Snake, Apples) +snakeAndApples = feedback DontEat $ proc (turn, eat) -> do + snake <- snakeSF -< (turn, eat) + (apples, eatNext) <- applesSF -< head $ body snake + returnA -< ((snake, apples), eatNext) + +-- | Whether a snake hits the boundaries or bites itself +illegal :: Snake -> Bool +illegal Snake {body = head@Position {x, y} :| tail} = + head `elem` tail + || x < (-boardSize) + || x > boardSize + || y < (-boardSize) + || y > boardSize + +game :: ClSF GlossConc GameClock Turn (Maybe (Snake, Apples)) +game = safely $ do + try $ liftClSF snakeAndApples >>> throwOnCond (fst >>> illegal) () >>> arr Just + safe $ pure Nothing + +render :: Maybe (Snake, Apples) -> Picture +render (Just (snake, apples)) = renderSnake snake <> foldMap renderApple apples +render Nothing = gameover + +gameover :: Picture +gameover = translate (-10) 0 $ scale 0.03 0.03 $ text "Game over!" + +-- | Scale and paint a gloss picture +visualize :: BehaviourF GlossConc UTCTime Picture () +visualize = arrMCl $ scale 10 10 >>> paintAllIO + +-- | Draw at 30 FPS +type VisualizationClock = GlossClockUTC IO GlossSimClockIO + +visualizationClock :: VisualizationClock +visualizationClock = glossClockUTC GlossSimClockIO + +-- | Select only those input events that correspond to turns of the snake +type UserClock = GlossClockUTC IO (SelectClock GlossEventClockIO Turn) + +userClock :: UserClock +userClock = + glossClockUTC $ + SelectClock + { mainClock = GlossEventClockIO + , select = \case + (EventKey (SpecialKey KeyRight) Down _ _) -> Just TurnRight + (EventKey (SpecialKey KeyLeft) Down _ _) -> Just TurnLeft + _ -> Nothing + } + +-- | User input to turn the snake +user :: ClSF GlossConc UserClock () Turn +user = tagS + +rhine = user @@ userClock >-- fifoBounded 1000 --> (arr (fromMaybe Stay) >-> game >-> arr render @@ gameClock) >-- keepLast mempty --> visualize @@ visualizationClock + +main :: IO () +-- Make sure to keep this definition here as it is: The tests depend on it. +main = flowGlossIO defaultSettings rhine diff --git a/koans/ui/1-gloss/5-randomness/solution/Koan.hs b/koans/ui/1-gloss/5-randomness/solution/Koan.hs new file mode 100644 index 0000000..2fa195a --- /dev/null +++ b/koans/ui/1-gloss/5-randomness/solution/Koan.hs @@ -0,0 +1,204 @@ +{-# LANGUAGE Arrows #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE TemplateHaskell #-} + +-- | Randomness. +module Koan where + +-- base +import Data.List.NonEmpty hiding (insert, unfold) +import Data.Maybe (fromMaybe) +import GHC.Generics + +-- random +import System.Random + +-- MonadRandom +import Control.Monad.Random + +-- containers +import Data.Set hiding (toList) + +-- rhine +import FRP.Rhine + +-- rhine-gloss +import FRP.Rhine.Gloss +import System.Random.Stateful (UniformRange (..)) +import Prelude hiding (head) + +-- * Grid positions on the playing board + +boardSize :: Int +boardSize = 20 + +data Position = Position + { x :: Int + , y :: Int + } + deriving (Generic, Eq, Ord) + +instance Semigroup Position where + Position x1 y1 <> Position x2 y2 = Position (x1 + x2) (y1 + y2) + +instance Monoid Position where + mempty = Position 0 0 + +-- | To generate random apple positions +instance Uniform Position + +instance UniformRange Position where + uniformRM (Position xLow yLow, Position xHigh yHigh) g = Position <$> uniformRM (xLow, xHigh) g <*> uniformRM (yLow, yHigh) g + +instance Random Position + +renderPosition :: Position -> Picture +renderPosition Position {x, y} = translate (fromIntegral x) (fromIntegral y) $ circleSolid 0.6 + +-- * Directions in which the snake can head + +data Direction = East | North | West | South + deriving (Enum) + +-- | A position changes by a direction in one step +stepPosition :: Direction -> Position -> Position +stepPosition East = (<> Position 1 0) +stepPosition North = (<> Position 0 1) +stepPosition West = (<> Position (-1) 0) +stepPosition South = (<> Position 0 (-1)) + +-- | The user can change the direction of the snake +data Turn + = -- | Don't change the direction. This happens if no key is pressed. + Stay + | -- | Turn right (clockwise) when the right arrow is pressed. + TurnRight + | -- | Turn left (counterclockwise) when the left arrow is pressed. + TurnLeft + deriving (Show) + +changeDirection :: Turn -> Direction -> Direction +changeDirection Stay direction = direction +changeDirection TurnRight direction = toEnum $ (fromEnum direction - 1) `mod` 4 +changeDirection TurnLeft direction = toEnum $ (fromEnum direction + 1) `mod` 4 + +-- | Whether the snake currently eats an apple. +data Eat = Eat | DontEat + +data Snake = Snake + { direction :: Direction + , body :: NonEmpty Position + } + +-- | A small snake. +snek :: Direction -> Position -> Snake +snek direction tinyBody = + Snake + { direction + , body = pure tinyBody + } + +-- | On every step, a snake can make a turn, and possibly eat an apple +stepSnake :: Turn -> Eat -> Snake -> Snake +stepSnake turn eat snake = + let + newDirection = changeDirection turn $ direction snake + newHead = stepPosition newDirection $ Data.List.NonEmpty.head $ body snake + newTail = tailAfterMeal eat snake + in + Snake + { direction = newDirection + , body = newHead :| newTail + } + where + tailAfterMeal :: Eat -> Snake -> [Position] + tailAfterMeal DontEat = Data.List.NonEmpty.init . body + tailAfterMeal Eat = toList . body + +renderSnake :: Snake -> Picture +renderSnake = foldMap renderPosition . body + +newtype Apple = Apple {getApple :: Position} + deriving (Eq, Ord) + +newApple :: (MonadRandom m) => ClSF m GameClock () (Maybe Apple) +newApple = proc _ -> do + nSteps :: Int <- count -< () + if nSteps `mod` 10 == 0 + then arr (Just <<< Apple) <<< getRandomRS -< (Position (-10) (-10), Position 10 10) + else returnA -< Nothing + +type Apples = Set Apple + +addAndEatApple :: + -- | Possibly a new apple appeared + Maybe Apple -> + -- | On this position the snake attempted to eat the apple + Position -> + -- | The previous collection of apples + Apples -> + (Apples, Eat) +addAndEatApple addedApple eatPosition oldApples = + let addedApples = maybe oldApples (`insert` oldApples) addedApple + newApples = delete (Apple eatPosition) addedApples + in (newApples, if size newApples < size addedApples then Eat else DontEat) + +renderApple :: Apple -> Picture +renderApple = color red . renderPosition . getApple + +type GameClock = GlossConcTClock IO (Millisecond 500) + +gameClock :: GameClock +gameClock = glossConcTClock waitClock + +snakeSF :: ClSF GlossConc GameClock (Turn, Eat) Snake +snakeSF = unfold_ (snek North mempty) $ \(turn, eat) s -> stepSnake turn eat s + +applesSF :: ClSF GlossConc GameClock Position (Apples, Eat) +applesSF = feedback empty $ proc (eatPosition, oldApples) -> do + addedApple <- evalRandIOS' newApple -< () + let (newApples, eat) = addAndEatApple addedApple eatPosition oldApples + returnA -< ((newApples, eat), newApples) + +game :: ClSF GlossConc GameClock Turn (Snake, Apples) +game = feedback DontEat $ proc (turn, eat) -> do + snake <- snakeSF -< (turn, eat) + (apples, eatNext) <- applesSF -< head $ body snake + returnA -< ((snake, apples), eatNext) + +render :: (Snake, Apples) -> Picture +render (snake, apples) = renderSnake snake <> foldMap renderApple apples + +-- | Scale and paint a gloss picture +visualize :: BehaviourF GlossConc UTCTime Picture () +visualize = arrMCl $ scale 10 10 >>> paintAllIO + +-- | Draw at 30 FPS +type VisualizationClock = GlossClockUTC IO GlossSimClockIO + +visualizationClock :: VisualizationClock +visualizationClock = glossClockUTC GlossSimClockIO + +-- | Select only those input events that correspond to turns of the snake +type UserClock = GlossClockUTC IO (SelectClock GlossEventClockIO Turn) + +userClock :: UserClock +userClock = + glossClockUTC $ + SelectClock + { mainClock = GlossEventClockIO + , select = \case + (EventKey (SpecialKey KeyRight) Down _ _) -> Just TurnRight + (EventKey (SpecialKey KeyLeft) Down _ _) -> Just TurnLeft + _ -> Nothing + } + +-- | User input to turn the snake +user :: ClSF GlossConc UserClock () Turn +user = tagS + +rhine = user @@ userClock >-- fifoBounded 1000 --> (arr (fromMaybe Stay) >-> game >-> arr render @@ gameClock) >-- keepLast mempty --> visualize @@ visualizationClock + +main :: IO () +-- Make sure to keep this definition here as it is: The tests depend on it. +main = flowGlossIO defaultSettings rhine diff --git a/koans/ui/1-gloss/5-randomness/test/Test.hs b/koans/ui/1-gloss/5-randomness/test/Test.hs new file mode 100644 index 0000000..a09fa81 --- /dev/null +++ b/koans/ui/1-gloss/5-randomness/test/Test.hs @@ -0,0 +1,38 @@ +module Main where + +-- base +import Control.Concurrent +import Control.Monad +import Data.IORef +import System.Exit + +-- transformers +import Control.Monad.Trans.Reader + +-- monad-schedule +import Control.Monad.Schedule.FreeAsync (runFreeAsyncT) + +-- rhine-gloss +import FRP.Rhine.Gloss + +-- koan +import Koan (rhine) + +flattenPictures :: Picture -> [Picture] +flattenPictures (Pictures ps) = ps >>= flattenPictures +flattenPictures Blank = [] +flattenPictures picture = [picture] + +main :: IO () +main = do + vars <- liftIO $ GlossEnv <$> newEmptyMVar <*> newEmptyMVar <*> newIORef Blank <*> newIORef 0 + void $ forkIO $ runFreeAsyncT $ runReaderT (unGlossConcT (flow rhine)) vars + putMVar (timeVar vars) 1 + putMVar (timeVar vars) 2 + threadDelay 100000 + pic <- readIORef (picRef vars) + when (flattenPictures pic /= [rectangleSolid 1 1]) $ do + print [rectangleSolid 1 1] + print pic + print $ flattenPictures pic + exitFailure diff --git a/koans/ui/1-gloss/6-control-flow/Koan.hs b/koans/ui/1-gloss/6-control-flow/Koan.hs new file mode 100644 index 0000000..ec0e85c --- /dev/null +++ b/koans/ui/1-gloss/6-control-flow/Koan.hs @@ -0,0 +1,221 @@ +{-# LANGUAGE Arrows #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE TemplateHaskell #-} + +module Koan where + +-- base +import Data.List.NonEmpty hiding (insert, unfold) +import Data.Maybe (fromMaybe) +import GHC.Generics + +-- random +import System.Random + +-- MonadRandom +import Control.Monad.Random + +-- containers +import Data.Set hiding (toList) + +-- rhine +import FRP.Rhine + +-- rhine-gloss +import FRP.Rhine.Gloss +import System.Random.Stateful (UniformRange (..)) +import Prelude hiding (head) + +-- * Grid positions on the playing board + +boardSize :: Int +boardSize = 20 + +data Position = Position + { x :: Int + , y :: Int + } + deriving (Generic, Eq, Ord) + +instance Semigroup Position where + Position x1 y1 <> Position x2 y2 = Position (x1 + x2) (y1 + y2) + +instance Monoid Position where + mempty = Position 0 0 + +-- | To generate random apple positions +instance Uniform Position + +instance UniformRange Position where + uniformRM (Position xLow yLow, Position xHigh yHigh) g = Position <$> uniformRM (xLow, xHigh) g <*> uniformRM (yLow, yHigh) g + +instance Random Position + +renderPosition :: Position -> Picture +renderPosition Position {x, y} = translate (fromIntegral x) (fromIntegral y) $ circleSolid 0.6 + +-- * Directions in which the snake can head + +data Direction = East | North | West | South + deriving (Enum) + +-- | A position changes by a direction in one step +stepPosition :: Direction -> Position -> Position +stepPosition East = (<> Position 1 0) +stepPosition North = (<> Position 0 1) +stepPosition West = (<> Position (-1) 0) +stepPosition South = (<> Position 0 (-1)) + +-- | The user can change the direction of the snake +data Turn + = -- | Don't change the direction. This happens if no key is pressed. + Stay + | -- | Turn right (clockwise) when the right arrow is pressed. + TurnRight + | -- | Turn left (counterclockwise) when the left arrow is pressed. + TurnLeft + deriving (Show) + +changeDirection :: Turn -> Direction -> Direction +changeDirection Stay direction = direction +changeDirection TurnRight direction = toEnum $ (fromEnum direction - 1) `mod` 4 +changeDirection TurnLeft direction = toEnum $ (fromEnum direction + 1) `mod` 4 + +-- | Whether the snake currently eats an apple. +data Eat = Eat | DontEat + +data Snake = Snake + { direction :: Direction + , body :: NonEmpty Position + } + +-- | A small snake. +snek :: Direction -> Position -> Snake +snek direction tinyBody = + Snake + { direction + , body = pure tinyBody + } + +-- | On every step, a snake can make a turn, and possibly eat an apple +stepSnake :: Turn -> Eat -> Snake -> Snake +stepSnake turn eat snake = + let + newDirection = changeDirection turn $ direction snake + newHead = stepPosition newDirection $ Data.List.NonEmpty.head $ body snake + newTail = tailAfterMeal eat snake + in + Snake + { direction = newDirection + , body = newHead :| newTail + } + where + tailAfterMeal :: Eat -> Snake -> [Position] + tailAfterMeal DontEat = Data.List.NonEmpty.init . body + tailAfterMeal Eat = toList . body + +renderSnake :: Snake -> Picture +renderSnake = foldMap renderPosition . body + +newtype Apple = Apple {getApple :: Position} + deriving (Eq, Ord) + +newApple :: (MonadRandom m) => ClSF m GameClock () (Maybe Apple) +newApple = proc _ -> do + nSteps :: Int <- count -< () + if nSteps `mod` 10 == 0 + then arr (Just <<< Apple) <<< getRandomRS -< (Position (-10) (-10), Position 10 10) + else returnA -< Nothing + +type Apples = Set Apple + +addAndEatApple :: + -- | Possibly a new apple appeared + Maybe Apple -> + -- | On this position the snake attempted to eat the apple + Position -> + -- | The previous collection of apples + Apples -> + (Apples, Eat) +addAndEatApple addedApple eatPosition oldApples = + let addedApples = maybe oldApples (`insert` oldApples) addedApple + newApples = delete (Apple eatPosition) addedApples + in (newApples, if size newApples < size addedApples then Eat else DontEat) + +renderApple :: Apple -> Picture +renderApple = color red . renderPosition . getApple + +type GameClock = GlossConcTClock IO (Millisecond 500) + +gameClock :: GameClock +gameClock = glossConcTClock waitClock + +snakeSF :: ClSF GlossConc GameClock (Turn, Eat) Snake +snakeSF = unfold (snek North mempty) $ \(turn, eat) s -> let s' = stepSnake turn eat s in Result s' s' + +applesSF :: ClSF GlossConc GameClock Position (Apples, Eat) +applesSF = feedback empty $ proc (eatPosition, oldApples) -> do + addedApple <- evalRandIOS' newApple -< () + let (newApples, eat) = addAndEatApple addedApple eatPosition oldApples + returnA -< ((newApples, eat), newApples) + +snakeAndApples :: ClSF GlossConc GameClock Turn (Snake, Apples) +snakeAndApples = feedback DontEat $ proc (turn, eat) -> do + snake <- snakeSF -< (turn, eat) + (apples, eatNext) <- applesSF -< head $ body snake + returnA -< ((snake, apples), eatNext) + +-- | Whether a snake hits the boundaries or bites itself +illegal :: Snake -> Bool +illegal Snake {body = head@Position {x, y} :| tail} = + head `elem` tail + || x < (-boardSize) + || x > boardSize + || y < (-boardSize) + || y > boardSize + +game :: ClSF GlossConc GameClock Turn (Maybe (Snake, Apples)) +game = safely $ do + try $ liftClSF snakeAndApples >>> throwOnCond (fst >>> illegal) () >>> arr Just + safe $ pure Nothing + +render :: Maybe (Snake, Apples) -> Picture +render (Just (snake, apples)) = renderSnake snake <> foldMap renderApple apples +render Nothing = gameover + +gameover :: Picture +gameover = translate (-10) 0 $ scale 0.03 0.03 $ text "Game over!" + +-- | Scale and paint a gloss picture +visualize :: BehaviourF GlossConc UTCTime Picture () +visualize = arrMCl $ scale 10 10 >>> paintAllIO + +-- | Draw at 30 FPS +type VisualizationClock = GlossClockUTC IO GlossSimClockIO + +visualizationClock :: VisualizationClock +visualizationClock = glossClockUTC GlossSimClockIO + +-- | Select only those input events that correspond to turns of the snake +type UserClock = GlossClockUTC IO (SelectClock GlossEventClockIO Turn) + +userClock :: UserClock +userClock = + glossClockUTC $ + SelectClock + { mainClock = GlossEventClockIO + , select = \case + (EventKey (SpecialKey KeyRight) Down _ _) -> Just TurnRight + (EventKey (SpecialKey KeyLeft) Down _ _) -> Just TurnLeft + _ -> Nothing + } + +-- | User input to turn the snake +user :: ClSF GlossConc UserClock () Turn +user = tagS + +rhine = user @@ userClock >-- fifoBounded 1000 --> (arr (fromMaybe Stay) >-> game >-> arr render @@ gameClock) >-- keepLast mempty --> visualize @@ visualizationClock + +main :: IO () +-- Make sure to keep this definition here as it is: The tests depend on it. +main = flowGlossIO defaultSettings rhine diff --git a/koans/ui/1-gloss/6-control-flow/solution/Koan.hs b/koans/ui/1-gloss/6-control-flow/solution/Koan.hs new file mode 100644 index 0000000..38c24c5 --- /dev/null +++ b/koans/ui/1-gloss/6-control-flow/solution/Koan.hs @@ -0,0 +1,221 @@ +{-# LANGUAGE Arrows #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE TemplateHaskell #-} + +module Koan where + +-- base +import Data.List.NonEmpty hiding (insert, unfold) +import Data.Maybe (fromMaybe) +import GHC.Generics + +-- random +import System.Random + +-- MonadRandom +import Control.Monad.Random + +-- containers +import Data.Set hiding (toList) + +-- rhine +import FRP.Rhine + +-- rhine-gloss +import FRP.Rhine.Gloss +import System.Random.Stateful (UniformRange (..)) +import Prelude hiding (head) + +-- * Grid positions on the playing board + +boardSize :: Int +boardSize = 20 + +data Position = Position + { x :: Int + , y :: Int + } + deriving (Generic, Eq, Ord) + +instance Semigroup Position where + Position x1 y1 <> Position x2 y2 = Position (x1 + x2) (y1 + y2) + +instance Monoid Position where + mempty = Position 0 0 + +-- | To generate random apple positions +instance Uniform Position + +instance UniformRange Position where + uniformRM (Position xLow yLow, Position xHigh yHigh) g = Position <$> uniformRM (xLow, xHigh) g <*> uniformRM (yLow, yHigh) g + +instance Random Position + +renderPosition :: Position -> Picture +renderPosition Position {x, y} = translate (fromIntegral x) (fromIntegral y) $ circleSolid 0.6 + +-- * Directions in which the snake can head + +data Direction = East | North | West | South + deriving (Enum) + +-- | A position changes by a direction in one step +stepPosition :: Direction -> Position -> Position +stepPosition East = (<> Position 1 0) +stepPosition North = (<> Position 0 1) +stepPosition West = (<> Position (-1) 0) +stepPosition South = (<> Position 0 (-1)) + +-- | The user can change the direction of the snake +data Turn + = -- | Don't change the direction. This happens if no key is pressed. + Stay + | -- | Turn right (clockwise) when the right arrow is pressed. + TurnRight + | -- | Turn left (counterclockwise) when the left arrow is pressed. + TurnLeft + deriving (Show) + +changeDirection :: Turn -> Direction -> Direction +changeDirection Stay direction = direction +changeDirection TurnRight direction = toEnum $ (fromEnum direction - 1) `mod` 4 +changeDirection TurnLeft direction = toEnum $ (fromEnum direction + 1) `mod` 4 + +-- | Whether the snake currently eats an apple. +data Eat = Eat | DontEat + +data Snake = Snake + { direction :: Direction + , body :: NonEmpty Position + } + +-- | A small snake. +snek :: Direction -> Position -> Snake +snek direction tinyBody = + Snake + { direction + , body = pure tinyBody + } + +-- | On every step, a snake can make a turn, and possibly eat an apple +stepSnake :: Turn -> Eat -> Snake -> Snake +stepSnake turn eat snake = + let + newDirection = changeDirection turn $ direction snake + newHead = stepPosition newDirection $ Data.List.NonEmpty.head $ body snake + newTail = tailAfterMeal eat snake + in + Snake + { direction = newDirection + , body = newHead :| newTail + } + where + tailAfterMeal :: Eat -> Snake -> [Position] + tailAfterMeal DontEat = Data.List.NonEmpty.init . body + tailAfterMeal Eat = toList . body + +renderSnake :: Snake -> Picture +renderSnake = foldMap renderPosition . body + +newtype Apple = Apple {getApple :: Position} + deriving (Eq, Ord) + +newApple :: (MonadRandom m) => ClSF m GameClock () (Maybe Apple) +newApple = proc _ -> do + nSteps :: Int <- count -< () + if nSteps `mod` 10 == 0 + then arr (Just <<< Apple) <<< getRandomRS -< (Position (-10) (-10), Position 10 10) + else returnA -< Nothing + +type Apples = Set Apple + +addAndEatApple :: + -- | Possibly a new apple appeared + Maybe Apple -> + -- | On this position the snake attempted to eat the apple + Position -> + -- | The previous collection of apples + Apples -> + (Apples, Eat) +addAndEatApple addedApple eatPosition oldApples = + let addedApples = maybe oldApples (`insert` oldApples) addedApple + newApples = delete (Apple eatPosition) addedApples + in (newApples, if size newApples < size addedApples then Eat else DontEat) + +renderApple :: Apple -> Picture +renderApple = color red . renderPosition . getApple + +type GameClock = GlossConcTClock IO (Millisecond 500) + +gameClock :: GameClock +gameClock = glossConcTClock waitClock + +snakeSF :: ClSF GlossConc GameClock (Turn, Eat) Snake +snakeSF = unfold_ (snek North mempty) $ \(turn, eat) s -> stepSnake turn eat s + +applesSF :: ClSF GlossConc GameClock Position (Apples, Eat) +applesSF = feedback empty $ proc (eatPosition, oldApples) -> do + addedApple <- evalRandIOS' newApple -< () + let (newApples, eat) = addAndEatApple addedApple eatPosition oldApples + returnA -< ((newApples, eat), newApples) + +snakeAndApples :: ClSF GlossConc GameClock Turn (Snake, Apples) +snakeAndApples = feedback DontEat $ proc (turn, eat) -> do + snake <- snakeSF -< (turn, eat) + (apples, eatNext) <- applesSF -< head $ body snake + returnA -< ((snake, apples), eatNext) + +-- | Whether a snake hits the boundaries or bites itself +illegal :: Snake -> Bool +illegal Snake {body = head@Position {x, y} :| tail} = + head `elem` tail + || x < (-boardSize) + || x > boardSize + || y < (-boardSize) + || y > boardSize + +game :: ClSF GlossConc GameClock Turn (Maybe (Snake, Apples)) +game = safely $ do + try $ liftClSF snakeAndApples >>> throwOnCond (fst >>> illegal) () >>> arr Just + safe $ pure Nothing + +render :: Maybe (Snake, Apples) -> Picture +render (Just (snake, apples)) = renderSnake snake <> foldMap renderApple apples +render Nothing = gameover + +gameover :: Picture +gameover = translate (-10) 0 $ scale 0.03 0.03 $ text "Game over!" + +-- | Scale and paint a gloss picture +visualize :: BehaviourF GlossConc UTCTime Picture () +visualize = arrMCl $ scale 10 10 >>> paintAllIO + +-- | Draw at 30 FPS +type VisualizationClock = GlossClockUTC IO GlossSimClockIO + +visualizationClock :: VisualizationClock +visualizationClock = glossClockUTC GlossSimClockIO + +-- | Select only those input events that correspond to turns of the snake +type UserClock = GlossClockUTC IO (SelectClock GlossEventClockIO Turn) + +userClock :: UserClock +userClock = + glossClockUTC $ + SelectClock + { mainClock = GlossEventClockIO + , select = \case + (EventKey (SpecialKey KeyRight) Down _ _) -> Just TurnRight + (EventKey (SpecialKey KeyLeft) Down _ _) -> Just TurnLeft + _ -> Nothing + } + +-- | User input to turn the snake +user :: ClSF GlossConc UserClock () Turn +user = tagS + +rhine = user @@ userClock >-- fifoBounded 1000 --> (arr (fromMaybe Stay) >-> game >-> arr render @@ gameClock) >-- keepLast mempty --> visualize @@ visualizationClock + +main :: IO () +-- Make sure to keep this definition here as it is: The tests depend on it. +main = flowGlossIO defaultSettings rhine diff --git a/koans/ui/1-gloss/6-control-flow/test/Test.hs b/koans/ui/1-gloss/6-control-flow/test/Test.hs new file mode 100644 index 0000000..a09fa81 --- /dev/null +++ b/koans/ui/1-gloss/6-control-flow/test/Test.hs @@ -0,0 +1,38 @@ +module Main where + +-- base +import Control.Concurrent +import Control.Monad +import Data.IORef +import System.Exit + +-- transformers +import Control.Monad.Trans.Reader + +-- monad-schedule +import Control.Monad.Schedule.FreeAsync (runFreeAsyncT) + +-- rhine-gloss +import FRP.Rhine.Gloss + +-- koan +import Koan (rhine) + +flattenPictures :: Picture -> [Picture] +flattenPictures (Pictures ps) = ps >>= flattenPictures +flattenPictures Blank = [] +flattenPictures picture = [picture] + +main :: IO () +main = do + vars <- liftIO $ GlossEnv <$> newEmptyMVar <*> newEmptyMVar <*> newIORef Blank <*> newIORef 0 + void $ forkIO $ runFreeAsyncT $ runReaderT (unGlossConcT (flow rhine)) vars + putMVar (timeVar vars) 1 + putMVar (timeVar vars) 2 + threadDelay 100000 + pic <- readIORef (picRef vars) + when (flattenPictures pic /= [rectangleSolid 1 1]) $ do + print [rectangleSolid 1 1] + print pic + print $ flattenPictures pic + exitFailure diff --git a/koans/ui/1-gloss/snake/Koan.hs b/koans/ui/1-gloss/snake/Koan.hs new file mode 100644 index 0000000..ec0e85c --- /dev/null +++ b/koans/ui/1-gloss/snake/Koan.hs @@ -0,0 +1,221 @@ +{-# LANGUAGE Arrows #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE TemplateHaskell #-} + +module Koan where + +-- base +import Data.List.NonEmpty hiding (insert, unfold) +import Data.Maybe (fromMaybe) +import GHC.Generics + +-- random +import System.Random + +-- MonadRandom +import Control.Monad.Random + +-- containers +import Data.Set hiding (toList) + +-- rhine +import FRP.Rhine + +-- rhine-gloss +import FRP.Rhine.Gloss +import System.Random.Stateful (UniformRange (..)) +import Prelude hiding (head) + +-- * Grid positions on the playing board + +boardSize :: Int +boardSize = 20 + +data Position = Position + { x :: Int + , y :: Int + } + deriving (Generic, Eq, Ord) + +instance Semigroup Position where + Position x1 y1 <> Position x2 y2 = Position (x1 + x2) (y1 + y2) + +instance Monoid Position where + mempty = Position 0 0 + +-- | To generate random apple positions +instance Uniform Position + +instance UniformRange Position where + uniformRM (Position xLow yLow, Position xHigh yHigh) g = Position <$> uniformRM (xLow, xHigh) g <*> uniformRM (yLow, yHigh) g + +instance Random Position + +renderPosition :: Position -> Picture +renderPosition Position {x, y} = translate (fromIntegral x) (fromIntegral y) $ circleSolid 0.6 + +-- * Directions in which the snake can head + +data Direction = East | North | West | South + deriving (Enum) + +-- | A position changes by a direction in one step +stepPosition :: Direction -> Position -> Position +stepPosition East = (<> Position 1 0) +stepPosition North = (<> Position 0 1) +stepPosition West = (<> Position (-1) 0) +stepPosition South = (<> Position 0 (-1)) + +-- | The user can change the direction of the snake +data Turn + = -- | Don't change the direction. This happens if no key is pressed. + Stay + | -- | Turn right (clockwise) when the right arrow is pressed. + TurnRight + | -- | Turn left (counterclockwise) when the left arrow is pressed. + TurnLeft + deriving (Show) + +changeDirection :: Turn -> Direction -> Direction +changeDirection Stay direction = direction +changeDirection TurnRight direction = toEnum $ (fromEnum direction - 1) `mod` 4 +changeDirection TurnLeft direction = toEnum $ (fromEnum direction + 1) `mod` 4 + +-- | Whether the snake currently eats an apple. +data Eat = Eat | DontEat + +data Snake = Snake + { direction :: Direction + , body :: NonEmpty Position + } + +-- | A small snake. +snek :: Direction -> Position -> Snake +snek direction tinyBody = + Snake + { direction + , body = pure tinyBody + } + +-- | On every step, a snake can make a turn, and possibly eat an apple +stepSnake :: Turn -> Eat -> Snake -> Snake +stepSnake turn eat snake = + let + newDirection = changeDirection turn $ direction snake + newHead = stepPosition newDirection $ Data.List.NonEmpty.head $ body snake + newTail = tailAfterMeal eat snake + in + Snake + { direction = newDirection + , body = newHead :| newTail + } + where + tailAfterMeal :: Eat -> Snake -> [Position] + tailAfterMeal DontEat = Data.List.NonEmpty.init . body + tailAfterMeal Eat = toList . body + +renderSnake :: Snake -> Picture +renderSnake = foldMap renderPosition . body + +newtype Apple = Apple {getApple :: Position} + deriving (Eq, Ord) + +newApple :: (MonadRandom m) => ClSF m GameClock () (Maybe Apple) +newApple = proc _ -> do + nSteps :: Int <- count -< () + if nSteps `mod` 10 == 0 + then arr (Just <<< Apple) <<< getRandomRS -< (Position (-10) (-10), Position 10 10) + else returnA -< Nothing + +type Apples = Set Apple + +addAndEatApple :: + -- | Possibly a new apple appeared + Maybe Apple -> + -- | On this position the snake attempted to eat the apple + Position -> + -- | The previous collection of apples + Apples -> + (Apples, Eat) +addAndEatApple addedApple eatPosition oldApples = + let addedApples = maybe oldApples (`insert` oldApples) addedApple + newApples = delete (Apple eatPosition) addedApples + in (newApples, if size newApples < size addedApples then Eat else DontEat) + +renderApple :: Apple -> Picture +renderApple = color red . renderPosition . getApple + +type GameClock = GlossConcTClock IO (Millisecond 500) + +gameClock :: GameClock +gameClock = glossConcTClock waitClock + +snakeSF :: ClSF GlossConc GameClock (Turn, Eat) Snake +snakeSF = unfold (snek North mempty) $ \(turn, eat) s -> let s' = stepSnake turn eat s in Result s' s' + +applesSF :: ClSF GlossConc GameClock Position (Apples, Eat) +applesSF = feedback empty $ proc (eatPosition, oldApples) -> do + addedApple <- evalRandIOS' newApple -< () + let (newApples, eat) = addAndEatApple addedApple eatPosition oldApples + returnA -< ((newApples, eat), newApples) + +snakeAndApples :: ClSF GlossConc GameClock Turn (Snake, Apples) +snakeAndApples = feedback DontEat $ proc (turn, eat) -> do + snake <- snakeSF -< (turn, eat) + (apples, eatNext) <- applesSF -< head $ body snake + returnA -< ((snake, apples), eatNext) + +-- | Whether a snake hits the boundaries or bites itself +illegal :: Snake -> Bool +illegal Snake {body = head@Position {x, y} :| tail} = + head `elem` tail + || x < (-boardSize) + || x > boardSize + || y < (-boardSize) + || y > boardSize + +game :: ClSF GlossConc GameClock Turn (Maybe (Snake, Apples)) +game = safely $ do + try $ liftClSF snakeAndApples >>> throwOnCond (fst >>> illegal) () >>> arr Just + safe $ pure Nothing + +render :: Maybe (Snake, Apples) -> Picture +render (Just (snake, apples)) = renderSnake snake <> foldMap renderApple apples +render Nothing = gameover + +gameover :: Picture +gameover = translate (-10) 0 $ scale 0.03 0.03 $ text "Game over!" + +-- | Scale and paint a gloss picture +visualize :: BehaviourF GlossConc UTCTime Picture () +visualize = arrMCl $ scale 10 10 >>> paintAllIO + +-- | Draw at 30 FPS +type VisualizationClock = GlossClockUTC IO GlossSimClockIO + +visualizationClock :: VisualizationClock +visualizationClock = glossClockUTC GlossSimClockIO + +-- | Select only those input events that correspond to turns of the snake +type UserClock = GlossClockUTC IO (SelectClock GlossEventClockIO Turn) + +userClock :: UserClock +userClock = + glossClockUTC $ + SelectClock + { mainClock = GlossEventClockIO + , select = \case + (EventKey (SpecialKey KeyRight) Down _ _) -> Just TurnRight + (EventKey (SpecialKey KeyLeft) Down _ _) -> Just TurnLeft + _ -> Nothing + } + +-- | User input to turn the snake +user :: ClSF GlossConc UserClock () Turn +user = tagS + +rhine = user @@ userClock >-- fifoBounded 1000 --> (arr (fromMaybe Stay) >-> game >-> arr render @@ gameClock) >-- keepLast mempty --> visualize @@ visualizationClock + +main :: IO () +-- Make sure to keep this definition here as it is: The tests depend on it. +main = flowGlossIO defaultSettings rhine diff --git a/koans/ui/1-gloss/snake/solution/Koan.hs b/koans/ui/1-gloss/snake/solution/Koan.hs new file mode 100644 index 0000000..38c24c5 --- /dev/null +++ b/koans/ui/1-gloss/snake/solution/Koan.hs @@ -0,0 +1,221 @@ +{-# LANGUAGE Arrows #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE TemplateHaskell #-} + +module Koan where + +-- base +import Data.List.NonEmpty hiding (insert, unfold) +import Data.Maybe (fromMaybe) +import GHC.Generics + +-- random +import System.Random + +-- MonadRandom +import Control.Monad.Random + +-- containers +import Data.Set hiding (toList) + +-- rhine +import FRP.Rhine + +-- rhine-gloss +import FRP.Rhine.Gloss +import System.Random.Stateful (UniformRange (..)) +import Prelude hiding (head) + +-- * Grid positions on the playing board + +boardSize :: Int +boardSize = 20 + +data Position = Position + { x :: Int + , y :: Int + } + deriving (Generic, Eq, Ord) + +instance Semigroup Position where + Position x1 y1 <> Position x2 y2 = Position (x1 + x2) (y1 + y2) + +instance Monoid Position where + mempty = Position 0 0 + +-- | To generate random apple positions +instance Uniform Position + +instance UniformRange Position where + uniformRM (Position xLow yLow, Position xHigh yHigh) g = Position <$> uniformRM (xLow, xHigh) g <*> uniformRM (yLow, yHigh) g + +instance Random Position + +renderPosition :: Position -> Picture +renderPosition Position {x, y} = translate (fromIntegral x) (fromIntegral y) $ circleSolid 0.6 + +-- * Directions in which the snake can head + +data Direction = East | North | West | South + deriving (Enum) + +-- | A position changes by a direction in one step +stepPosition :: Direction -> Position -> Position +stepPosition East = (<> Position 1 0) +stepPosition North = (<> Position 0 1) +stepPosition West = (<> Position (-1) 0) +stepPosition South = (<> Position 0 (-1)) + +-- | The user can change the direction of the snake +data Turn + = -- | Don't change the direction. This happens if no key is pressed. + Stay + | -- | Turn right (clockwise) when the right arrow is pressed. + TurnRight + | -- | Turn left (counterclockwise) when the left arrow is pressed. + TurnLeft + deriving (Show) + +changeDirection :: Turn -> Direction -> Direction +changeDirection Stay direction = direction +changeDirection TurnRight direction = toEnum $ (fromEnum direction - 1) `mod` 4 +changeDirection TurnLeft direction = toEnum $ (fromEnum direction + 1) `mod` 4 + +-- | Whether the snake currently eats an apple. +data Eat = Eat | DontEat + +data Snake = Snake + { direction :: Direction + , body :: NonEmpty Position + } + +-- | A small snake. +snek :: Direction -> Position -> Snake +snek direction tinyBody = + Snake + { direction + , body = pure tinyBody + } + +-- | On every step, a snake can make a turn, and possibly eat an apple +stepSnake :: Turn -> Eat -> Snake -> Snake +stepSnake turn eat snake = + let + newDirection = changeDirection turn $ direction snake + newHead = stepPosition newDirection $ Data.List.NonEmpty.head $ body snake + newTail = tailAfterMeal eat snake + in + Snake + { direction = newDirection + , body = newHead :| newTail + } + where + tailAfterMeal :: Eat -> Snake -> [Position] + tailAfterMeal DontEat = Data.List.NonEmpty.init . body + tailAfterMeal Eat = toList . body + +renderSnake :: Snake -> Picture +renderSnake = foldMap renderPosition . body + +newtype Apple = Apple {getApple :: Position} + deriving (Eq, Ord) + +newApple :: (MonadRandom m) => ClSF m GameClock () (Maybe Apple) +newApple = proc _ -> do + nSteps :: Int <- count -< () + if nSteps `mod` 10 == 0 + then arr (Just <<< Apple) <<< getRandomRS -< (Position (-10) (-10), Position 10 10) + else returnA -< Nothing + +type Apples = Set Apple + +addAndEatApple :: + -- | Possibly a new apple appeared + Maybe Apple -> + -- | On this position the snake attempted to eat the apple + Position -> + -- | The previous collection of apples + Apples -> + (Apples, Eat) +addAndEatApple addedApple eatPosition oldApples = + let addedApples = maybe oldApples (`insert` oldApples) addedApple + newApples = delete (Apple eatPosition) addedApples + in (newApples, if size newApples < size addedApples then Eat else DontEat) + +renderApple :: Apple -> Picture +renderApple = color red . renderPosition . getApple + +type GameClock = GlossConcTClock IO (Millisecond 500) + +gameClock :: GameClock +gameClock = glossConcTClock waitClock + +snakeSF :: ClSF GlossConc GameClock (Turn, Eat) Snake +snakeSF = unfold_ (snek North mempty) $ \(turn, eat) s -> stepSnake turn eat s + +applesSF :: ClSF GlossConc GameClock Position (Apples, Eat) +applesSF = feedback empty $ proc (eatPosition, oldApples) -> do + addedApple <- evalRandIOS' newApple -< () + let (newApples, eat) = addAndEatApple addedApple eatPosition oldApples + returnA -< ((newApples, eat), newApples) + +snakeAndApples :: ClSF GlossConc GameClock Turn (Snake, Apples) +snakeAndApples = feedback DontEat $ proc (turn, eat) -> do + snake <- snakeSF -< (turn, eat) + (apples, eatNext) <- applesSF -< head $ body snake + returnA -< ((snake, apples), eatNext) + +-- | Whether a snake hits the boundaries or bites itself +illegal :: Snake -> Bool +illegal Snake {body = head@Position {x, y} :| tail} = + head `elem` tail + || x < (-boardSize) + || x > boardSize + || y < (-boardSize) + || y > boardSize + +game :: ClSF GlossConc GameClock Turn (Maybe (Snake, Apples)) +game = safely $ do + try $ liftClSF snakeAndApples >>> throwOnCond (fst >>> illegal) () >>> arr Just + safe $ pure Nothing + +render :: Maybe (Snake, Apples) -> Picture +render (Just (snake, apples)) = renderSnake snake <> foldMap renderApple apples +render Nothing = gameover + +gameover :: Picture +gameover = translate (-10) 0 $ scale 0.03 0.03 $ text "Game over!" + +-- | Scale and paint a gloss picture +visualize :: BehaviourF GlossConc UTCTime Picture () +visualize = arrMCl $ scale 10 10 >>> paintAllIO + +-- | Draw at 30 FPS +type VisualizationClock = GlossClockUTC IO GlossSimClockIO + +visualizationClock :: VisualizationClock +visualizationClock = glossClockUTC GlossSimClockIO + +-- | Select only those input events that correspond to turns of the snake +type UserClock = GlossClockUTC IO (SelectClock GlossEventClockIO Turn) + +userClock :: UserClock +userClock = + glossClockUTC $ + SelectClock + { mainClock = GlossEventClockIO + , select = \case + (EventKey (SpecialKey KeyRight) Down _ _) -> Just TurnRight + (EventKey (SpecialKey KeyLeft) Down _ _) -> Just TurnLeft + _ -> Nothing + } + +-- | User input to turn the snake +user :: ClSF GlossConc UserClock () Turn +user = tagS + +rhine = user @@ userClock >-- fifoBounded 1000 --> (arr (fromMaybe Stay) >-> game >-> arr render @@ gameClock) >-- keepLast mempty --> visualize @@ visualizationClock + +main :: IO () +-- Make sure to keep this definition here as it is: The tests depend on it. +main = flowGlossIO defaultSettings rhine diff --git a/koans/ui/1-gloss/snake/test/Test.hs b/koans/ui/1-gloss/snake/test/Test.hs new file mode 100644 index 0000000..a09fa81 --- /dev/null +++ b/koans/ui/1-gloss/snake/test/Test.hs @@ -0,0 +1,38 @@ +module Main where + +-- base +import Control.Concurrent +import Control.Monad +import Data.IORef +import System.Exit + +-- transformers +import Control.Monad.Trans.Reader + +-- monad-schedule +import Control.Monad.Schedule.FreeAsync (runFreeAsyncT) + +-- rhine-gloss +import FRP.Rhine.Gloss + +-- koan +import Koan (rhine) + +flattenPictures :: Picture -> [Picture] +flattenPictures (Pictures ps) = ps >>= flattenPictures +flattenPictures Blank = [] +flattenPictures picture = [picture] + +main :: IO () +main = do + vars <- liftIO $ GlossEnv <$> newEmptyMVar <*> newEmptyMVar <*> newIORef Blank <*> newIORef 0 + void $ forkIO $ runFreeAsyncT $ runReaderT (unGlossConcT (flow rhine)) vars + putMVar (timeVar vars) 1 + putMVar (timeVar vars) 2 + threadDelay 100000 + pic <- readIORef (picRef vars) + when (flattenPictures pic /= [rectangleSolid 1 1]) $ do + print [rectangleSolid 1 1] + print pic + print $ flattenPictures pic + exitFailure diff --git a/koans/wsml/Koan.hs b/koans/wsml/Koan.hs new file mode 100644 index 0000000..8eb4378 --- /dev/null +++ b/koans/wsml/Koan.hs @@ -0,0 +1,26 @@ +module Koan where + +import Wuss + +import Control.Concurrent (forkIO, threadDelay) +import Control.Monad (forever, unless, void) +import Data.Text (Text, pack) +import Network.WebSockets (ClientApp, receiveData, sendClose, sendTextData) + +main :: IO () +-- main = runSecureClient "echo.websocket.org" 443 "/" ws +main = runSecureClient "www.seismicportal.eu" 443 "/standing_order/websocket" ws +-- main = runSecureClient "www.seismicportal.eu" 443 "/" ws -- try again at home +-- main = runSecureClient "api2.poloniex.com" 443 "/" ws + +ws :: ClientApp () +ws connection = do + putStrLn "Connected!" + + void . forkIO . forever $ do + message <- receiveData connection + print (message :: Text) + + threadDelay $ 1000 * 1000000 + + sendClose connection (pack "Bye!") diff --git a/koans/wsml/KoanWS.hs b/koans/wsml/KoanWS.hs new file mode 100644 index 0000000..a6773e5 --- /dev/null +++ b/koans/wsml/KoanWS.hs @@ -0,0 +1,35 @@ +-------------------------------------------------------------------------------- +{-# LANGUAGE OverloadedStrings #-} + +module Koan where + +-------------------------------------------------------------------------------- +import Control.Concurrent (forkIO) +import Control.Monad (forever, unless) +import Control.Monad.IO.Class +import Data.Text (Text) +import qualified Data.Text as T +import qualified Data.Text.IO as T +import qualified Network.WebSockets as WS + +-------------------------------------------------------------------------------- +app :: WS.ClientApp () +app conn = do + putStrLn "Connected!" + + -- Fork a thread that writes WS data to stdout + _ <- forkIO $ forever $ do + msg <- WS.receiveData conn + liftIO $ T.putStrLn msg + + -- Read from stdin and write to WS + let loop = do + line <- T.getLine + unless (T.null line) $ WS.sendTextData conn line >> loop + + loop + WS.sendClose conn ("Bye!" :: Text) + +-------------------------------------------------------------------------------- +main :: IO () +main = WS.runClient "www.seismicportal.eu" 80 "/standing_order" app diff --git a/rhine-koans.cabal b/rhine-koans.cabal index 30b4a1c..b08902b 100644 --- a/rhine-koans.cabal +++ b/rhine-koans.cabal @@ -58,6 +58,14 @@ library test-io silently ^>= 1.2 , temporary ^>= 1.3 +library test-gloss + import: opts + hs-source-dirs: generic/test-gloss + exposed-modules: TestGloss + build-depends: + rhine-gloss, + monad-schedule ^>= 0.2 + common basic-1-1-hello-rhine if flag(solution) hs-source-dirs: koans/basic/1/1-hello-rhine/solution @@ -329,3 +337,142 @@ test-suite basic-2-9-modularize-test type: exitcode-stdio-1.0 main-is: Test.hs hs-source-dirs: koans/basic/2/9-modularize/test + +common gloss + build-depends: + rhine-gloss, + transformers ^>= 0.6, + random ^>= 1.2, + MonadRandom ^>= 0.6, + containers ^>= 0.6, + monad-schedule ^>= 0.2, + rhine-koans:test-gloss + +common ui-1-gloss-1-circle + import: gloss + if flag(solution) + hs-source-dirs: koans/ui/1-gloss/1-circle/solution + else + hs-source-dirs: koans/ui/1-gloss/1-circle + +executable ui-1-gloss-1-circle + import: exec, ui-1-gloss-1-circle + main-is: Main.hs + +test-suite ui-1-gloss-1-circle-test + import: test, ui-1-gloss-1-circle + type: exitcode-stdio-1.0 + main-is: Test.hs + hs-source-dirs: koans/ui/1-gloss/1-circle/test + +common ui-1-gloss-2-move + import: gloss + if flag(solution) + hs-source-dirs: koans/ui/1-gloss/2-move/solution + else + hs-source-dirs: koans/ui/1-gloss/2-move + +executable ui-1-gloss-2-move + import: exec, ui-1-gloss-2-move + main-is: Main.hs + +test-suite ui-1-gloss-2-move-test + import: test, ui-1-gloss-2-move + type: exitcode-stdio-1.0 + main-is: Test.hs + hs-source-dirs: koans/ui/1-gloss/2-move/test + +common ui-1-gloss-3-modularize + import: gloss + if flag(solution) + hs-source-dirs: koans/ui/1-gloss/3-modularize/solution + else + hs-source-dirs: koans/ui/1-gloss/3-modularize + +executable ui-1-gloss-3-modularize + import: exec, ui-1-gloss-3-modularize + main-is: Main.hs + +test-suite ui-1-gloss-3-modularize-test + import: test, ui-1-gloss-3-modularize + type: exitcode-stdio-1.0 + main-is: Test.hs + hs-source-dirs: koans/ui/1-gloss/3-modularize/test + +common ui-1-gloss-4-user-input + import: gloss + if flag(solution) + hs-source-dirs: koans/ui/1-gloss/4-user-input/solution + else + hs-source-dirs: koans/ui/1-gloss/4-user-input + +executable ui-1-gloss-4-user-input + import: exec, ui-1-gloss-4-user-input + main-is: Main.hs + +test-suite ui-1-gloss-4-user-input-test + import: test, ui-1-gloss-4-user-input + type: exitcode-stdio-1.0 + main-is: Test.hs + hs-source-dirs: koans/ui/1-gloss/4-user-input/test + +common ui-1-gloss-5-randomness + import: gloss + if flag(solution) + hs-source-dirs: koans/ui/1-gloss/5-randomness/solution + else + hs-source-dirs: koans/ui/1-gloss/5-randomness + +executable ui-1-gloss-5-randomness + import: exec, ui-1-gloss-5-randomness + main-is: Main.hs + +test-suite ui-1-gloss-5-randomness-test + import: test, ui-1-gloss-5-randomness + type: exitcode-stdio-1.0 + main-is: Test.hs + hs-source-dirs: koans/ui/1-gloss/5-randomness/test + +common ui-1-gloss-6-control-flow + import: gloss + if flag(solution) + hs-source-dirs: koans/ui/1-gloss/6-control-flow/solution + else + hs-source-dirs: koans/ui/1-gloss/6-control-flow + +executable ui-1-gloss-6-control-flow + import: exec, ui-1-gloss-6-control-flow + main-is: Main.hs + +test-suite ui-1-gloss-6-control-flow-test + import: test, ui-1-gloss-6-control-flow + type: exitcode-stdio-1.0 + main-is: Test.hs + hs-source-dirs: koans/ui/1-gloss/6-control-flow/test + +common ui-1-gloss-snake + import: gloss + if flag(solution) + hs-source-dirs: koans/ui/1-gloss/snake/solution + else + hs-source-dirs: koans/ui/1-gloss/snake + build-depends: + automaton ^>= 1.3, + +executable ui-1-gloss-snake + import: exec, ui-1-gloss-snake + main-is: Main.hs + +test-suite ui-1-gloss-snake-test + import: test, ui-1-gloss-snake + type: exitcode-stdio-1.0 + main-is: Test.hs + hs-source-dirs: koans/ui/1-gloss/snake/test + +executable wsml + import: exec + main-is: Main.hs + hs-source-dirs: koans/wsml + build-depends: + websockets ^>= 0.13, + wuss ^>= 2