packages feed

worldturtle-0.1.0.0: Graphics/WorldTurtle/Internal/Sequence.hs

{-# LANGUAGE TemplateHaskell #-}
{-# OPTIONS_HADDOCK hide #-}
{-# LANGUAGE BangPatterns #-}
module Graphics.WorldTurtle.Internal.Sequence
  ( Turtle 
  , TSC
  , SequenceCommand
  , AlmostVal
  , renderTurtle
  , addPicture
  , simTime
  , setSimTime
  , decrementSimTime
  , pics
  , totalSimTime
  , turtles
  , generateTurtle
  , animate'
  , animate
  , combineSequence
  , alternateSequence
  , failSequence
  ) where

import Graphics.WorldTurtle.Internal.Turtle

import Graphics.Gloss.Data.Picture (Picture, pictures)

import Control.Monad.Cont
import Control.Monad.State

import Control.Lens

import Data.Void (Void, absurd)
import Data.Maybe (isNothing, isJust)

import Data.Map.Strict (Map)
import qualified Data.Map.Strict as Map

-- | AlmostVal represents a computation that can "almost" complete. Either
--   There is enough time to solve the computation, or the computation needs
--   to exit early as there is not enough time to fully run the computation.
type AlmostVal a = Maybe a

-- | State Monad that takes our `TSC` type as its state object.
type TurtleState b = State (TSC b)

-- | Continuation Monad on top of the State Monad of form @SequenceCommand b a@.
--   /b/ is the final return type of the entire Monad sequence - this is what 
--   will be returned if/when we need to exit early from anywhere in a great big
--   sequence of steps. /a/ is the return type of the current step of the 
--   animation sequence. That is: what will be passed into the next step.
type SequenceCommand b a = ContT b (TurtleState b) a

-- Careful of editing the Turtle comment below as it is public docs!
-- Really "Turtle" is just a handle to internal TurtleData. It is a key that
-- looks up TurtleData in a map. Since Turtle is exposed to the user-level we 
-- do not document it in this way however.

-- | The Turtle that is drawn on the canvas! Create a new turtle using 
-- `Graphics.WorldTurtle.Commands.makeTurtle`.
newtype Turtle = Turtle Int deriving (Eq, Ord)

data TSC b = TSC
  { _pics :: ![Picture] -- ^ All pictures that make up the current canvas
  , _exitCall :: SequenceCommand b b -- ^ Stop drawing call for animations
  , _totalSimTime :: !Float -- ^ Remaining available for animating
  , _turtles :: Map Turtle TurtleData  -- Collection of all turtles.
  , _nextTurtleId :: !Int -- ^ ID of next turtle to be generated.
  }

$(makeLenses ''TSC)

-- | Generates default parameter arguments. The TSC returned by this value
-- must never be used for sequencing as the exitCall is undefined and will only
-- be defined in the setup stage of the animation process.
defaultTSC :: Float -> TSC b
defaultTSC givenTime = TSC 
           { _pics = []
           , _totalSimTime = givenTime
           , _exitCall = error "Exit called but not defined in animation."
           , _turtles = Map.empty
           , _nextTurtleId = 0
           }

-- | Gets the remaining simulation time of the current turtle process.
-- The simulation time dictates how much time is remaining for an animation,
-- and it will be reduced as the animations play in sequence. Once this value
-- hits 0 the exit command will be called and the monad will stop processing.
simTime :: SequenceCommand b Float
simTime = use totalSimTime

-- | Sets the simulation time in the state monad.
-- If the simulation time is <= 0 then this setter will immediately call the
-- exit function which will kill any further processing of the monad.
setSimTime :: Float -> SequenceCommand b ()
setSimTime newTime = do
  let newTime' = max 0 newTime
  totalSimTime .= newTime'
  when (newTime' <= 0) failSequence

-- | Takes a value away form the current sim time and store the updated time.
-- See `setSimTime`.
decrementSimTime :: Float -- ^ Value to subtract from store simulation time. 
                 -> SequenceCommand b ()
decrementSimTime duration = simTime >>= setSimTime . (flip (-) duration)

-- | Given a picture, adds it to the picture list.
addPicture :: Picture -- ^ Picture to add to our animation
           -> SequenceCommand b ()
addPicture p = pics %= (p :)

-- | Never call an animation directly, always call this instead!
-- This is part of our setup stage to inject the exit call into the animation
-- before running the animation. What is returned by this class is either
-- the completed animation or an early exit. 
--
-- We take our command and an exit call, and store the exit in the state monad 
-- then execute the command.
-- The return value is either a `Nothing` which means the exit was called early
-- or a `Just a` which is the monad successfully completed.
exitCondition :: SequenceCommand (AlmostVal a) a -- ^ Animation passed in.
              -> SequenceCommand (AlmostVal a) (AlmostVal a)
exitCondition commands = callCC $ \exit -> do
    exitCall .= exit Nothing
    decrementSimTime 0 -- In case we are already at a time of 0.
    Just <$> commands

processTurtle :: SequenceCommand (AlmostVal a) a 
              -> TSC (AlmostVal a)
              -> (AlmostVal a, TSC (AlmostVal a))
processTurtle commands tsc = 
  let drawS = runContT (exitCondition commands) return
   in runState drawS tsc

renderTurtle :: SequenceCommand (AlmostVal a) a -> Float ->  Picture
renderTurtle c f = let (_, s) = processTurtle c (defaultTSC f)
                    in pictures $ s ^. pics ++ drawTurtles (s ^. turtles)

drawTurtles :: Map Turtle TurtleData -> [Picture]
drawTurtles m = fmap drawTurtle $ Map.elems m 

generateTurtle :: SequenceCommand b Turtle
generateTurtle = do
  t <- Turtle <$> use nextTurtleId
  turtles %= Map.insert t defaultTurtle
  nextTurtleId += 1
  return t

animate' :: Float 
         -> Float 
         -> (Float -> SequenceCommand b a) 
         -> SequenceCommand b a
animate' !distance !turtleSpeed callback =
   let !duration = distance / turtleSpeed
       !d' = if isNaN duration || isInfinite duration then 0 else duration
       --  if speed is 0 we use this as a "no animation" command from 
       --   user-space.
     in animate (abs d') callback

animate :: Float -> (Float -> SequenceCommand b a) -> SequenceCommand b a
animate !duration callback = do
   timeRemaining <- simTime -- simulation time to go
   let !availableTime = min timeRemaining duration
   --  Amount of time we have to complete the animation before we need to exit.
   let !timeQuot = if availableTime == 0 then 1 else availableTime / duration
   --  quotient of available time vs required time. Note that when the duration
   --   is 0 we say "don't do any animation"
   t <- callback timeQuot 
   --  Perform the calculation with the quotient for lerping
   decrementSimTime availableTime 
   --  Test to see if this is the end of our animation and if so exit early
   return t

-- | Runs two items in parallel then applies a semigroup combination operator
--   to the result of both.
--   This combination can only return if both A and B return. Compare to 
--   `alternateSequence` which can return if one returns.
combineSequence :: Semigroup a
                => SequenceCommand b a -- ^ Sequence @a@ to run.
                -> SequenceCommand b a -- ^ Sequence @b@ to run.
                -> SequenceCommand b a 
                    -- ^ New sequence of A and B in parallel.
combineSequence a b = do
  (!aVal, !bVal) <- runParallel a b
  -- If either attempt failed, we fail also.
  when (isNothing aVal || isNothing bVal) failSequence

  -- Everything is hunky dory so we continue on into the next bind of the monad.
  let (Just !aVal') = aVal
  let (Just !bVal') = bVal
  return $ aVal' <> bVal'

-- | Runs two items in sequence, returns the result of `a` if `a` passes,
--   otherwise returns the results of `b`. The implication of this is that only
--   the result of a will be returned while animating, and b when animation is
--   finished.
alternateSequence :: SequenceCommand b a -- ^ Sequence @a@ to run.
                  -> SequenceCommand b a -- ^ Sequence @b@ to run.
                  -> SequenceCommand b a
alternateSequence a b = do
  (!aVal, !bVal) <- runParallel a b
  
  -- If both values failed we fail also.
  when (isNothing aVal && isNothing bVal) failSequence

  -- If A passes, return the value of A, otherwise return the value of B.
  if isJust aVal 
    then let (Just !aVal') = aVal in return $! aVal'
    else let (Just !bVal') = bVal in return $! bVal'

-- | Given two sequences @a@ and @b@, instead of running them both as separate 
--   animations, run them both in parallel!
runParallel :: SequenceCommand c a -- ^ Sequence @a@ to run.
            -> SequenceCommand c b -- ^ Sequence @b@ to run.
            -> SequenceCommand c (AlmostVal a, AlmostVal b)
               -- ^ New sequence of A and B which returns both results.
runParallel a b = do
  startSimTime <- use totalSimTime
  parentExitCall <- use exitCall

  -- Run A, and return back to this point when/if it fails.
  aVal <- callCC $ \ exitFromA -> do
    exitCall .= exitFromA Nothing
    Just <$> a

  aSimTime <- use totalSimTime
  
  -- Run B, and return back to this point when/if it fails.
  bVal <- callCC $ \ exitFromB -> do
    exitCall .= exitFromB Nothing
    totalSimTime .= startSimTime -- restart sim time back to initial.
    Just <$> b

  bSimTime <- use totalSimTime

  -- No subsequent animation can proceed until the longest animation completes.
  -- We take the remaining animation time to the remaining time of the longest 
  -- running animation.
  totalSimTime .= min aSimTime bSimTime

  exitCall .= parentExitCall  -- Let us exit properly again!

  -- Now we must test the remaining sim time. The above calls might have
  -- succeeded while still exhausting our remaining time -- which as far as
  -- animating is concerned is the same as not succeeding at all!
  decrementSimTime 0

  return $! (aVal, bVal)

-- | Calls our early exit and fails the callback. No calculations will be
--   performed beyond this call.
failSequence :: SequenceCommand b a
failSequence = do
  ex <- use exitCall
  _ <- ex
  -- We can never reach this point with our call to `ex`. So the return type
  -- can be whatever we want it to be. Let's go crazy! 
  let (Just x) = (Nothing :: Maybe Void)
   in absurd x