Add TimeEffect effect for getting current time (#1620)

fixes #1502

Add TimeEffect effect for getting current time.
This commit is contained in:
persik 2023-11-15 06:17:09 +00:00 committed by GitHub
parent 8f52e53a22
commit a306d05f61
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 74 additions and 30 deletions

6
src/Swarm/Effect.hs Normal file
View File

@ -0,0 +1,6 @@
module Swarm.Effect (
module X,
)
where
import Swarm.Effect.Time as X

27
src/Swarm/Effect/Time.hs Normal file
View File

@ -0,0 +1,27 @@
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE GeneralisedNewtypeDeriving #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE UndecidableInstances #-}
module Swarm.Effect.Time where
import Control.Algebra
import Control.Monad.Trans (MonadIO (liftIO))
import Data.Kind (Type)
import System.Clock (Clock (Monotonic), TimeSpec, getTime)
-- | Effect for things related to time
data Time (m :: Type -> Type) k where
GetNow :: Time m TimeSpec
getNow :: Has Time sig m => m TimeSpec
getNow = send GetNow
newtype TimeIOC m a = TimeIOC {runTimeIO :: m a}
deriving newtype (Applicative, Functor, Monad, MonadIO)
instance (MonadIO m, Algebra sig m) => Algebra (Time :+: sig) (TimeIOC m) where
alg hdl sig ctx = case sig of
L GetNow -> (<$ ctx) <$> liftIO (System.Clock.getTime System.Clock.Monotonic)
R other -> TimeIOC (alg (runTimeIO . hdl) other ctx)

View File

@ -59,6 +59,7 @@ import Data.Time (getZonedTime)
import Data.Tuple (swap) import Data.Tuple (swap)
import Linear (V2 (..), perp, zero) import Linear (V2 (..), perp, zero)
import Prettyprinter (pretty) import Prettyprinter (pretty)
import Swarm.Effect as Effect (Time, getNow)
import Swarm.Game.Achievement.Attainment import Swarm.Game.Achievement.Attainment
import Swarm.Game.Achievement.Definitions import Swarm.Game.Achievement.Definitions
import Swarm.Game.CESK import Swarm.Game.CESK
@ -116,7 +117,7 @@ import Prelude hiding (Applicative (..), lookup)
-- --
-- Note that the game may be in 'RobotStep' mode and not finish -- Note that the game may be in 'RobotStep' mode and not finish
-- the tick. Use the return value to check whether a full tick happened. -- the tick. Use the return value to check whether a full tick happened.
gameTick :: (Has (State GameState) sig m, Has (Lift IO) sig m) => m Bool gameTick :: (Has (State GameState) sig m, Has (Lift IO) sig m, Has Effect.Time sig m) => m Bool
gameTick = do gameTick = do
wakeUpRobotsDoneSleeping wakeUpRobotsDoneSleeping
active <- use activeRobots active <- use activeRobots
@ -164,7 +165,7 @@ gameTick = do
-- | Finish a game tick in progress and set the game to 'WorldTick' mode afterwards. -- | Finish a game tick in progress and set the game to 'WorldTick' mode afterwards.
-- --
-- Use this function if you need to unpause the game. -- Use this function if you need to unpause the game.
finishGameTick :: (Has (State GameState) sig m, Has (Lift IO) sig m) => m () finishGameTick :: (Has (State GameState) sig m, Has (Lift IO) sig m, Has Effect.Time sig m) => m ()
finishGameTick = finishGameTick =
use (temporal . gameStep) >>= \case use (temporal . gameStep) >>= \case
WorldTick -> pure () WorldTick -> pure ()
@ -189,7 +190,7 @@ insertBackRobot rn rob = do
unless (isActive rob) (sleepForever rn) unless (isActive rob) (sleepForever rn)
-- Run a set of robots - this is used to run robots before/after the focused one. -- Run a set of robots - this is used to run robots before/after the focused one.
runRobotIDs :: (Has (State GameState) sig m, Has (Lift IO) sig m) => IS.IntSet -> m () runRobotIDs :: (Has (State GameState) sig m, Has (Lift IO) sig m, Has Effect.Time sig m) => IS.IntSet -> m ()
runRobotIDs robotNames = forM_ (IS.toList robotNames) $ \rn -> do runRobotIDs robotNames = forM_ (IS.toList robotNames) $ \rn -> do
mr <- uses robotMap (IM.lookup rn) mr <- uses robotMap (IM.lookup rn)
forM_ mr (stepOneRobot rn) forM_ mr (stepOneRobot rn)
@ -197,7 +198,7 @@ runRobotIDs robotNames = forM_ (IS.toList robotNames) $ \rn -> do
stepOneRobot rn rob = tickRobot rob >>= insertBackRobot rn stepOneRobot rn rob = tickRobot rob >>= insertBackRobot rn
-- This is a helper function to do one robot step or run robots before/after. -- This is a helper function to do one robot step or run robots before/after.
singleStep :: (Has (State GameState) sig m, Has (Lift IO) sig m) => SingleStep -> RID -> IS.IntSet -> m Bool singleStep :: (Has (State GameState) sig m, Has (Lift IO) sig m, Has Effect.Time sig m) => SingleStep -> RID -> IS.IntSet -> m Bool
singleStep ss focRID robotSet = do singleStep ss focRID robotSet = do
let (preFoc, focusedActive, postFoc) = IS.splitMember focRID robotSet let (preFoc, focusedActive, postFoc) = IS.splitMember focRID robotSet
case ss of case ss of
@ -291,7 +292,7 @@ data CompletionsWithExceptions = CompletionsWithExceptions
-- 3) The iteration needs to be a "fold", so that state is updated -- 3) The iteration needs to be a "fold", so that state is updated
-- after each element. -- after each element.
hypotheticalWinCheck :: hypotheticalWinCheck ::
(Has (State GameState) sig m, Has (Lift IO) sig m) => (Has (State GameState) sig m, Has Effect.Time sig m, Has (Lift IO) sig m) =>
EntityMap -> EntityMap ->
GameState -> GameState ->
WinStatus -> WinStatus ->
@ -381,7 +382,11 @@ hypotheticalWinCheck em g ws oc = do
h = hypotheticalRobot (Out VUnit emptyStore []) 0 h = hypotheticalRobot (Out VUnit emptyStore []) 0
evalPT :: evalPT ::
(Has (Lift IO) sig m, Has (Throw Exn) sig m, Has (State GameState) sig m) => ( Has Effect.Time sig m
, Has (Throw Exn) sig m
, Has (State GameState) sig m
, Has (Lift IO) sig m
) =>
ProcessedTerm -> ProcessedTerm ->
m Value m Value
evalPT t = evaluateCESK (initMachine t empty emptyStore) evalPT t = evaluateCESK (initMachine t empty emptyStore)
@ -407,7 +412,11 @@ hypotheticalRobot c =
mempty mempty
evaluateCESK :: evaluateCESK ::
(Has (Lift IO) sig m, Has (Throw Exn) sig m, Has (State GameState) sig m) => ( Has Effect.Time sig m
, Has (Throw Exn) sig m
, Has (State GameState) sig m
, Has (Lift IO) sig m
) =>
CESK -> CESK ->
m Value m Value
evaluateCESK cesk = do evaluateCESK cesk = do
@ -417,7 +426,8 @@ evaluateCESK cesk = do
evalState r . runCESK $ cesk evalState r . runCESK $ cesk
runCESK :: runCESK ::
( Has (Lift IO) sig m ( Has Effect.Time sig m
, Has (Lift IO) sig m
, Has (Throw Exn) sig m , Has (Throw Exn) sig m
, Has (State GameState) sig m , Has (State GameState) sig m
, Has (State Robot) sig m , Has (State Robot) sig m
@ -520,7 +530,7 @@ withExceptions s k m = do
-- | Run a robot for one tick, which may consist of up to -- | Run a robot for one tick, which may consist of up to
-- 'robotStepsPerTick' CESK machine steps and at most one tangible -- 'robotStepsPerTick' CESK machine steps and at most one tangible
-- command execution, whichever comes first. -- command execution, whichever comes first.
tickRobot :: (Has (State GameState) sig m, Has (Lift IO) sig m) => Robot -> m Robot tickRobot :: (Has (State GameState) sig m, Has (Lift IO) sig m, Has Effect.Time sig m) => Robot -> m Robot
tickRobot r = do tickRobot r = do
steps <- use $ temporal . robotStepsPerTick steps <- use $ temporal . robotStepsPerTick
tickRobotRec (r & activityCounts . tickStepBudget .~ steps) tickRobotRec (r & activityCounts . tickStepBudget .~ steps)
@ -529,7 +539,7 @@ tickRobot r = do
-- robot is actively running and still has steps left, and if so -- robot is actively running and still has steps left, and if so
-- runs it for one step, then calls itself recursively to continue -- runs it for one step, then calls itself recursively to continue
-- stepping the robot. -- stepping the robot.
tickRobotRec :: (Has (State GameState) sig m, Has (Lift IO) sig m) => Robot -> m Robot tickRobotRec :: (Has (State GameState) sig m, Has (Lift IO) sig m, Has Effect.Time sig m) => Robot -> m Robot
tickRobotRec r = do tickRobotRec r = do
time <- use $ temporal . ticks time <- use $ temporal . ticks
case wantsToStep time r && (r ^. runningAtomic || r ^. activityCounts . tickStepBudget > 0) of case wantsToStep time r && (r ^. runningAtomic || r ^. activityCounts . tickStepBudget > 0) of
@ -538,7 +548,7 @@ tickRobotRec r = do
-- | Single-step a robot by decrementing its 'tickStepBudget' counter and -- | Single-step a robot by decrementing its 'tickStepBudget' counter and
-- running its CESK machine for one step. -- running its CESK machine for one step.
stepRobot :: (Has (State GameState) sig m, Has (Lift IO) sig m) => Robot -> m Robot stepRobot :: (Has (State GameState) sig m, Has (Lift IO) sig m, Has Effect.Time sig m) => Robot -> m Robot
stepRobot r = do stepRobot r = do
(r', cesk') <- runState (r & activityCounts . tickStepBudget -~ 1) (stepCESK (r ^. machine)) (r', cesk') <- runState (r & activityCounts . tickStepBudget -~ 1) (stepCESK (r ^. machine))
-- sendIO $ appendFile "out.txt" (prettyString cesk' ++ "\n") -- sendIO $ appendFile "out.txt" (prettyString cesk' ++ "\n")
@ -589,7 +599,7 @@ data SKpair = SKpair Store Cont
-- --
-- Compare to "withExceptions". -- Compare to "withExceptions".
processImmediateFrame :: processImmediateFrame ::
(Has (State GameState) sig m, Has (State Robot) sig m, Has (Lift IO) sig m) => (Has (State GameState) sig m, Has (State Robot) sig m, Has (Lift IO) sig m, Has Effect.Time sig m) =>
Value -> Value ->
SKpair -> SKpair ->
-- | the unreliable computation -- | the unreliable computation
@ -614,7 +624,7 @@ updateWorldAndRobots cmd wf rf = do
-- | The main CESK machine workhorse. Given a robot, look at its CESK -- | The main CESK machine workhorse. Given a robot, look at its CESK
-- machine state and figure out a single next step. -- machine state and figure out a single next step.
stepCESK :: (Has (State GameState) sig m, Has (State Robot) sig m, Has (Lift IO) sig m) => CESK -> m CESK stepCESK :: (Has (State GameState) sig m, Has (State Robot) sig m, Has (Lift IO) sig m, Has Effect.Time sig m) => CESK -> m CESK
stepCESK cesk = case cesk of stepCESK cesk = case cesk of
------------------------------------------------------------ ------------------------------------------------------------
-- Evaluation -- Evaluation
@ -963,7 +973,7 @@ stepCESK cesk = case cesk of
-- | Eexecute a constant, catching any exception thrown and returning -- | Eexecute a constant, catching any exception thrown and returning
-- it via a CESK machine state. -- it via a CESK machine state.
evalConst :: evalConst ::
(Has (State GameState) sig m, Has (State Robot) sig m, Has (Lift IO) sig m) => Const -> [Value] -> Store -> Cont -> m CESK (Has (State GameState) sig m, Has (State Robot) sig m, Has Effect.Time sig m, Has (Lift IO) sig m) => Const -> [Value] -> Store -> Cont -> m CESK
evalConst c vs s k = do evalConst c vs s k = do
res <- runError $ execConst c vs s k res <- runError $ execConst c vs s k
case res of case res of
@ -1021,7 +1031,7 @@ addSeedBot e (minT, maxT) loc ts =
-- | Interpret the execution (or evaluation) of a constant application -- | Interpret the execution (or evaluation) of a constant application
-- to some values. -- to some values.
execConst :: execConst ::
(HasRobotStepState sig m, Has (Lift IO) sig m) => (HasRobotStepState sig m, Has Effect.Time sig m, Has (Lift IO) sig m) =>
Const -> Const ->
[Value] -> [Value] ->
Store -> Store ->
@ -2550,7 +2560,7 @@ execConst c vs s k = do
-- The code for grab and harvest is almost identical, hence factored -- The code for grab and harvest is almost identical, hence factored
-- out here. -- out here.
doGrab :: (HasRobotStepState sig m, Has (Lift IO) sig m) => GrabbingCmd -> m Entity doGrab :: (HasRobotStepState sig m, Has Effect.Time sig m) => GrabbingCmd -> m Entity
doGrab cmd = do doGrab cmd = do
let verb = verbGrabbingCmd cmd let verb = verbGrabbingCmd cmd
verbed = verbedGrabbingCmd cmd verbed = verbedGrabbingCmd cmd

View File

@ -19,11 +19,11 @@ module Swarm.Game.Step.Combustion where
import Control.Applicative (Applicative (..)) import Control.Applicative (Applicative (..))
import Control.Carrier.State.Lazy import Control.Carrier.State.Lazy
import Control.Effect.Lens import Control.Effect.Lens
import Control.Effect.Lift
import Control.Lens as Lens hiding (Const, distrib, from, parts, use, uses, view, (%=), (+=), (.=), (<+=), (<>=)) import Control.Lens as Lens hiding (Const, distrib, from, parts, use, uses, view, (%=), (+=), (.=), (<+=), (<>=))
import Control.Monad (forM_, void, when) import Control.Monad (forM_, void, when)
import Data.Text qualified as T import Data.Text qualified as T
import Linear (zero) import Linear (zero)
import Swarm.Effect as Effect (Time, getNow)
import Swarm.Game.CESK (emptyStore, initMachine) import Swarm.Game.CESK (emptyStore, initMachine)
import Swarm.Game.Display import Swarm.Game.Display
import Swarm.Game.Entity hiding (empty, lookup, singleton, union) import Swarm.Game.Entity hiding (empty, lookup, singleton, union)
@ -44,7 +44,7 @@ import Swarm.Util hiding (both)
import System.Clock (TimeSpec) import System.Clock (TimeSpec)
import Prelude hiding (Applicative (..), lookup) import Prelude hiding (Applicative (..), lookup)
igniteCommand :: (HasRobotStepState sig m, Has (Lift IO) sig m) => Const -> Direction -> m () igniteCommand :: (HasRobotStepState sig m, Has Effect.Time sig m) => Const -> Direction -> m ()
igniteCommand c d = do igniteCommand c d = do
(loc, me) <- lookInDirection d (loc, me) <- lookInDirection d
-- Ensure there is an entity here. -- Ensure there is an entity here.

View File

@ -13,7 +13,6 @@ import Control.Applicative (Applicative (..))
import Control.Carrier.State.Lazy import Control.Carrier.State.Lazy
import Control.Effect.Error import Control.Effect.Error
import Control.Effect.Lens import Control.Effect.Lens
import Control.Effect.Lift
import Control.Monad (forM_, guard, when) import Control.Monad (forM_, guard, when)
import Control.Monad.Trans.Class (lift) import Control.Monad.Trans.Class (lift)
import Control.Monad.Trans.Maybe (MaybeT (..), runMaybeT) import Control.Monad.Trans.Maybe (MaybeT (..), runMaybeT)
@ -41,8 +40,6 @@ import Swarm.Language.Capability
import Swarm.Language.Requirement qualified as R import Swarm.Language.Requirement qualified as R
import Swarm.Language.Syntax import Swarm.Language.Syntax
import Swarm.Util hiding (both) import Swarm.Util hiding (both)
import System.Clock (TimeSpec)
import System.Clock qualified
import System.Random (UniformRange, uniformR) import System.Random (UniformRange, uniformR)
import Prelude hiding (Applicative (..), lookup) import Prelude hiding (Applicative (..), lookup)
@ -115,9 +112,6 @@ cmdExn c parts = CmdFailed c (T.unwords parts) Nothing
-- * Some utility functions -- * Some utility functions
getNow :: Has (Lift IO) sig m => m TimeSpec
getNow = sendIO $ System.Clock.getTime System.Clock.Monotonic
-- | Set a flag telling the UI that the world needs to be redrawn. -- | Set a flag telling the UI that the world needs to be redrawn.
flagRedraw :: (Has (State GameState) sig m) => m () flagRedraw :: (Has (State GameState) sig m) => m ()
flagRedraw = needsRedraw .= True flagRedraw = needsRedraw .= True

View File

@ -70,6 +70,7 @@ import Data.Time (getZonedTime)
import Data.Vector qualified as V import Data.Vector qualified as V
import Graphics.Vty qualified as V import Graphics.Vty qualified as V
import Linear import Linear
import Swarm.Effect (TimeIOC (..))
import Swarm.Game.Achievement.Definitions import Swarm.Game.Achievement.Definitions
import Swarm.Game.Achievement.Persistence import Swarm.Game.Achievement.Persistence
import Swarm.Game.CESK (CESK (Out), Frame (FApp, FExec), cancel, emptyStore, initMachine) import Swarm.Game.CESK (CESK (Out), Frame (FApp, FExec), cancel, emptyStore, initMachine)
@ -751,10 +752,10 @@ runGameTickUI :: EventM Name AppState ()
runGameTickUI = runGameTick >> void updateUI runGameTickUI = runGameTick >> void updateUI
-- | Modifies the game state using a fused-effect state action. -- | Modifies the game state using a fused-effect state action.
zoomGameState :: (MonadState AppState m, MonadIO m) => Fused.StateC GameState (Fused.LiftC IO) a -> m a zoomGameState :: (MonadState AppState m, MonadIO m) => Fused.StateC GameState (TimeIOC (Fused.LiftC IO)) a -> m a
zoomGameState f = do zoomGameState f = do
gs <- use gameState gs <- use gameState
(gs', a) <- liftIO (Fused.runM (Fused.runState gs f)) (gs', a) <- liftIO (Fused.runM (runTimeIO (Fused.runState gs f)))
gameState .= gs' gameState .= gs'
return a return a

View File

@ -4,6 +4,7 @@
-- fused-effect utilities for Swarm. -- fused-effect utilities for Swarm.
module Swarm.Util.Effect where module Swarm.Util.Effect where
import Control.Algebra
import Control.Carrier.Accum.FixedStrict import Control.Carrier.Accum.FixedStrict
import Control.Carrier.Error.Either (ErrorC (..)) import Control.Carrier.Error.Either (ErrorC (..))
import Control.Carrier.Throw.Either (ThrowC (..), runThrow) import Control.Carrier.Throw.Either (ThrowC (..), runThrow)

View File

@ -125,6 +125,8 @@ library
Swarm.Game.Scenario Swarm.Game.Scenario
Swarm.Game.Scenario.Topography.Cell Swarm.Game.Scenario.Topography.Cell
Swarm.Game.Universe Swarm.Game.Universe
Swarm.Effect
Swarm.Effect.Time
Swarm.Log Swarm.Log
Swarm.TUI.Launch.Controller Swarm.TUI.Launch.Controller
Swarm.TUI.Launch.Model Swarm.TUI.Launch.Model

View File

@ -10,6 +10,7 @@ import Control.Monad (replicateM_)
import Control.Monad.Except (runExceptT) import Control.Monad.Except (runExceptT)
import Control.Monad.State (evalStateT, execStateT) import Control.Monad.State (evalStateT, execStateT)
import Data.Map qualified as M import Data.Map qualified as M
import Swarm.Effect (runTimeIO)
import Swarm.Game.CESK (emptyStore, initMachine) import Swarm.Game.CESK (emptyStore, initMachine)
import Swarm.Game.Display (defaultRobotDisplay) import Swarm.Game.Display (defaultRobotDisplay)
import Swarm.Game.Location import Swarm.Game.Location
@ -127,7 +128,7 @@ mkGameState robotMaker numRobots = do
-- | Runs numGameTicks ticks of the game. -- | Runs numGameTicks ticks of the game.
runGame :: Int -> GameState -> IO () runGame :: Int -> GameState -> IO ()
runGame numGameTicks = evalStateT (replicateM_ numGameTicks gameTick) runGame numGameTicks = evalStateT (replicateM_ numGameTicks $ runTimeIO gameTick)
main :: IO () main :: IO ()
main = do main = do

View File

@ -28,6 +28,7 @@ import Data.Text.IO qualified as T
import Data.Yaml (ParseException, prettyPrintParseException) import Data.Yaml (ParseException, prettyPrintParseException)
import Swarm.Doc.Gen (EditorType (..)) import Swarm.Doc.Gen (EditorType (..))
import Swarm.Doc.Gen qualified as DocGen import Swarm.Doc.Gen qualified as DocGen
import Swarm.Effect (runTimeIO)
import Swarm.Game.Achievement.Definitions (GameplayAchievement (..)) import Swarm.Game.Achievement.Definitions (GameplayAchievement (..))
import Swarm.Game.CESK (emptyStore, getTickNumber, initMachine) import Swarm.Game.CESK (emptyStore, getTickNumber, initMachine)
import Swarm.Game.Entity (EntityMap, lookupByName) import Swarm.Game.Entity (EntityMap, lookupByName)
@ -484,7 +485,7 @@ testScenarioSolutions rs ui =
b <- gets badErrorsInLogs b <- gets badErrorsInLogs
when (null b) $ case w of when (null b) $ case w of
WinConditions (Won _) _ -> return () WinConditions (Won _) _ -> return ()
_ -> gameTick >> playUntilWin _ -> runTimeIO gameTick >> playUntilWin
noBadErrors :: GameState -> Assertion noBadErrors :: GameState -> Assertion
noBadErrors g = do noBadErrors g = do

View File

@ -13,6 +13,7 @@ import Control.Monad.State (StateT (..), execState)
import Control.Monad.Trans (lift) import Control.Monad.Trans (lift)
import Data.Text (Text) import Data.Text (Text)
import Data.Text qualified as T import Data.Text qualified as T
import Swarm.Effect
import Swarm.Game.CESK import Swarm.Game.CESK
import Swarm.Game.Exception import Swarm.Game.Exception
import Swarm.Game.Robot import Swarm.Game.Robot
@ -47,7 +48,7 @@ runCESK :: Int -> CESK -> StateT Robot (StateT GameState IO) (Either Text (Value
runCESK _ (Up exn _ []) = Left . flip formatExn exn <$> lift (use $ landscape . entityMap) runCESK _ (Up exn _ []) = Left . flip formatExn exn <$> lift (use $ landscape . entityMap)
runCESK !steps cesk = case finalValue cesk of runCESK !steps cesk = case finalValue cesk of
Just (v, _) -> return (Right (v, steps)) Just (v, _) -> return (Right (v, steps))
Nothing -> stepCESK cesk >>= runCESK (steps + 1) Nothing -> runTimeIO (stepCESK cesk) >>= runCESK (steps + 1)
play :: GameState -> Text -> IO (Either Text (), GameState) play :: GameState -> Text -> IO (Either Text (), GameState)
play g = either (return . (,g) . Left) playPT . processTerm1 play g = either (return . (,g) . Left) playPT . processTerm1
@ -68,7 +69,7 @@ playUntilDone rid = do
w <- use robotMap w <- use robotMap
case w ^? ix rid . to isActive of case w ^? ix rid . to isActive of
Just True -> do Just True -> do
void gameTick void $ runTimeIO gameTick
playUntilDone rid playUntilDone rid
Just False -> return $ Right () Just False -> return $ Right ()
Nothing -> return $ Left . T.pack $ "The robot with ID " <> show rid <> " is nowhere to be found!" Nothing -> return $ Left . T.pack $ "The robot with ID " <> show rid <> " is nowhere to be found!"