swarm 0.2.0.0 → 0.3.0.0
raw patch · 183 files changed
+11542/−3008 lines, 183 filesdep +text-ropedep ~brickdep ~lineardep ~lspPVP ok
version bump matches the API change (PVP)
Dependencies added: text-rope
Dependency ranges changed: brick, linear, lsp, megaparsec
API changes (from Hackage documentation)
- Swarm.Game.CESK: RobotUpdate :: (Inventory -> Inventory) -> RobotUpdate
- Swarm.Game.CESK: WorldUpdate :: (World Int Entity -> Either Exn (World Int Entity)) -> WorldUpdate
- Swarm.Game.CESK: [robotUpdateInventory] :: RobotUpdate -> Inventory -> Inventory
- Swarm.Game.CESK: [worldUpdate] :: WorldUpdate -> World Int Entity -> Either Exn (World Int Entity)
- Swarm.Game.CESK: instance Data.Aeson.Types.FromJSON.FromJSON Swarm.Game.CESK.WorldUpdate
- Swarm.Game.CESK: instance Data.Aeson.Types.ToJSON.ToJSON Swarm.Game.CESK.WorldUpdate
- Swarm.Game.CESK: instance GHC.Classes.Eq Swarm.Game.CESK.WorldUpdate
- Swarm.Game.CESK: instance GHC.Show.Show Swarm.Game.CESK.WorldUpdate
- Swarm.Game.CESK: newtype RobotUpdate
- Swarm.Game.CESK: newtype WorldUpdate
- Swarm.Game.CESK: prettyCESK :: CESK -> String
- Swarm.Game.CESK: prettyCont :: Cont -> String
- Swarm.Game.CESK: prettyFrame :: Frame -> String
- Swarm.Game.CESK: type Loc = Int
- Swarm.Game.Exception: FixByInstall :: IncapableFix
- Swarm.Game.Robot: ErrorTrace :: LogSource
- Swarm.Game.Robot: LogEntry :: Integer -> LogSource -> Text -> Int -> V2 Int64 -> Text -> LogEntry
- Swarm.Game.Robot: Logged :: LogSource
- Swarm.Game.Robot: Said :: LogSource
- Swarm.Game.Robot: [_leLocation] :: LogEntry -> V2 Int64
- Swarm.Game.Robot: [_leRobotID] :: LogEntry -> Int
- Swarm.Game.Robot: [_leRobotName] :: LogEntry -> Text
- Swarm.Game.Robot: [_leSaid] :: LogEntry -> LogSource
- Swarm.Game.Robot: [_leText] :: LogEntry -> Text
- Swarm.Game.Robot: [_leTime] :: LogEntry -> Integer
- Swarm.Game.Robot: data LogEntry
- Swarm.Game.Robot: data LogSource
- Swarm.Game.Robot: installedDevices :: Lens' Robot Inventory
- Swarm.Game.Robot: instance Data.Aeson.Types.FromJSON.FromJSON Swarm.Game.Robot.LogEntry
- Swarm.Game.Robot: instance Data.Aeson.Types.FromJSON.FromJSON Swarm.Game.Robot.LogSource
- Swarm.Game.Robot: instance Data.Aeson.Types.ToJSON.ToJSON Swarm.Game.Robot.LogEntry
- Swarm.Game.Robot: instance Data.Aeson.Types.ToJSON.ToJSON Swarm.Game.Robot.LogSource
- Swarm.Game.Robot: instance GHC.Classes.Eq Swarm.Game.Robot.LogEntry
- Swarm.Game.Robot: instance GHC.Classes.Eq Swarm.Game.Robot.LogSource
- Swarm.Game.Robot: instance GHC.Classes.Ord Swarm.Game.Robot.LogEntry
- Swarm.Game.Robot: instance GHC.Classes.Ord Swarm.Game.Robot.LogSource
- Swarm.Game.Robot: instance GHC.Generics.Generic Swarm.Game.Robot.LogEntry
- Swarm.Game.Robot: instance GHC.Generics.Generic Swarm.Game.Robot.LogSource
- Swarm.Game.Robot: instance GHC.Show.Show Swarm.Game.Robot.LogEntry
- Swarm.Game.Robot: instance GHC.Show.Show Swarm.Game.Robot.LogSource
- Swarm.Game.Robot: leLocation :: Lens' LogEntry (V2 Int64)
- Swarm.Game.Robot: leRobotID :: Lens' LogEntry Int
- Swarm.Game.Robot: leRobotName :: Lens' LogEntry Text
- Swarm.Game.Robot: leSaid :: Lens' LogEntry LogSource
- Swarm.Game.Robot: leText :: Lens' LogEntry Text
- Swarm.Game.Robot: leTime :: Lens' LogEntry Integer
- Swarm.Game.Scenario: data Cell
- Swarm.Game.Scenario: data Objective
- Swarm.Game.Scenario: data WorldDescription
- Swarm.Game.Scenario: instance Data.Aeson.Types.FromJSON.FromJSON Swarm.Game.Scenario.Objective
- Swarm.Game.Scenario: instance Data.Aeson.Types.ToJSON.ToJSON Swarm.Game.Scenario.Objective
- Swarm.Game.Scenario: instance GHC.Classes.Eq Swarm.Game.Scenario.Cell
- Swarm.Game.Scenario: instance GHC.Classes.Eq Swarm.Game.Scenario.Objective
- Swarm.Game.Scenario: instance GHC.Classes.Eq Swarm.Game.Scenario.WorldDescription
- Swarm.Game.Scenario: instance GHC.Classes.Eq Swarm.Game.Scenario.WorldPalette
- Swarm.Game.Scenario: instance GHC.Generics.Generic Swarm.Game.Scenario.Objective
- Swarm.Game.Scenario: instance GHC.Show.Show Swarm.Game.Scenario.Cell
- Swarm.Game.Scenario: instance GHC.Show.Show Swarm.Game.Scenario.Objective
- Swarm.Game.Scenario: instance GHC.Show.Show Swarm.Game.Scenario.WorldDescription
- Swarm.Game.Scenario: instance GHC.Show.Show Swarm.Game.Scenario.WorldPalette
- Swarm.Game.Scenario: instance Swarm.Util.Yaml.FromJSONE (Swarm.Game.Entity.EntityMap, Swarm.Game.Scenario.RobotMap) Swarm.Game.Scenario.Cell
- Swarm.Game.Scenario: instance Swarm.Util.Yaml.FromJSONE (Swarm.Game.Entity.EntityMap, Swarm.Game.Scenario.RobotMap) Swarm.Game.Scenario.WorldDescription
- Swarm.Game.Scenario: instance Swarm.Util.Yaml.FromJSONE (Swarm.Game.Entity.EntityMap, Swarm.Game.Scenario.RobotMap) Swarm.Game.Scenario.WorldPalette
- Swarm.Game.Scenario: objectiveCondition :: Lens' Objective ProcessedTerm
- Swarm.Game.Scenario: objectiveGoal :: Lens' Objective [Text]
- Swarm.Game.State: _Won :: Prism' WinCondition Bool
- Swarm.Game.Value: [VBind] :: Maybe Var -> Term -> Term -> Env -> Value
- Swarm.Game.Value: [VBool] :: Bool -> Value
- Swarm.Game.Value: [VCApp] :: Const -> [Value] -> Value
- Swarm.Game.Value: [VClo] :: Var -> Term -> Env -> Value
- Swarm.Game.Value: [VDef] :: Bool -> Var -> Term -> Env -> Value
- Swarm.Game.Value: [VDelay] :: Term -> Env -> Value
- Swarm.Game.Value: [VDir] :: Direction -> Value
- Swarm.Game.Value: [VInj] :: Bool -> Value -> Value
- Swarm.Game.Value: [VInt] :: Integer -> Value
- Swarm.Game.Value: [VPair] :: Value -> Value -> Value
- Swarm.Game.Value: [VRef] :: Int -> Value
- Swarm.Game.Value: [VResult] :: Value -> Env -> Value
- Swarm.Game.Value: [VRobot] :: Int -> Value
- Swarm.Game.Value: [VText] :: Text -> Value
- Swarm.Game.Value: [VUnit] :: Value
- Swarm.Game.Value: data Value
- Swarm.Game.Value: instance Data.Aeson.Types.FromJSON.FromJSON Swarm.Game.Value.Value
- Swarm.Game.Value: instance Data.Aeson.Types.ToJSON.ToJSON Swarm.Game.Value.Value
- Swarm.Game.Value: instance GHC.Classes.Eq Swarm.Game.Value.Value
- Swarm.Game.Value: instance GHC.Generics.Generic Swarm.Game.Value.Value
- Swarm.Game.Value: instance GHC.Show.Show Swarm.Game.Value.Value
- Swarm.Game.Value: prettyValue :: Value -> Text
- Swarm.Game.Value: type Env = Ctx Value
- Swarm.Game.Value: valueToTerm :: Value -> Term
- Swarm.Language.Capability: CInstall :: Capability
- Swarm.Language.Syntax: Install :: Const
- Swarm.Language.Syntax: Installed :: Const
- Swarm.Language.Syntax: Location :: Int -> Int -> Location
- Swarm.Language.Syntax: Syntax :: Location -> Term -> Syntax
- Swarm.Language.Syntax: [dirAbs] :: DirInfo -> Maybe (V2 Int64)
- Swarm.Language.Syntax: [sLoc] :: Syntax -> Location
- Swarm.Language.Syntax: [sTerm] :: Syntax -> Term
- Swarm.Language.Syntax: data Location
- Swarm.Language.Syntax: data Syntax
- Swarm.Language.Syntax: data Term
- Swarm.Language.Syntax: fv :: Traversal' Term Var
- Swarm.Language.Syntax: fvT :: Traversal' Term Term
- Swarm.Language.Syntax: instance Control.Lens.Plated.Plated Swarm.Language.Syntax.Term
- Swarm.Language.Syntax: instance Data.Aeson.Types.FromJSON.FromJSON Swarm.Language.Syntax.Location
- Swarm.Language.Syntax: instance Data.Aeson.Types.FromJSON.FromJSON Swarm.Language.Syntax.Syntax
- Swarm.Language.Syntax: instance Data.Aeson.Types.FromJSON.FromJSON Swarm.Language.Syntax.Term
- Swarm.Language.Syntax: instance Data.Aeson.Types.FromJSON.FromJSONKey Swarm.Language.Syntax.Direction
- Swarm.Language.Syntax: instance Data.Aeson.Types.ToJSON.ToJSON Swarm.Language.Syntax.Location
- Swarm.Language.Syntax: instance Data.Aeson.Types.ToJSON.ToJSON Swarm.Language.Syntax.Syntax
- Swarm.Language.Syntax: instance Data.Aeson.Types.ToJSON.ToJSON Swarm.Language.Syntax.Term
- Swarm.Language.Syntax: instance Data.Aeson.Types.ToJSON.ToJSONKey Swarm.Language.Syntax.Direction
- Swarm.Language.Syntax: instance Data.Data.Data Swarm.Language.Syntax.Location
- Swarm.Language.Syntax: instance Data.Data.Data Swarm.Language.Syntax.Syntax
- Swarm.Language.Syntax: instance Data.Data.Data Swarm.Language.Syntax.Term
- Swarm.Language.Syntax: instance GHC.Base.Monoid Swarm.Language.Syntax.Location
- Swarm.Language.Syntax: instance GHC.Base.Semigroup Swarm.Language.Syntax.Location
- Swarm.Language.Syntax: instance GHC.Classes.Eq Swarm.Language.Syntax.Location
- Swarm.Language.Syntax: instance GHC.Classes.Eq Swarm.Language.Syntax.Syntax
- Swarm.Language.Syntax: instance GHC.Classes.Eq Swarm.Language.Syntax.Term
- Swarm.Language.Syntax: instance GHC.Enum.Bounded Swarm.Language.Syntax.Direction
- Swarm.Language.Syntax: instance GHC.Enum.Enum Swarm.Language.Syntax.Direction
- Swarm.Language.Syntax: instance GHC.Generics.Generic Swarm.Language.Syntax.Location
- Swarm.Language.Syntax: instance GHC.Generics.Generic Swarm.Language.Syntax.Syntax
- Swarm.Language.Syntax: instance GHC.Generics.Generic Swarm.Language.Syntax.Term
- Swarm.Language.Syntax: instance GHC.Show.Show Swarm.Language.Syntax.Location
- Swarm.Language.Syntax: instance GHC.Show.Show Swarm.Language.Syntax.Syntax
- Swarm.Language.Syntax: instance GHC.Show.Show Swarm.Language.Syntax.Term
- Swarm.Language.Syntax: mapFree1 :: Var -> (Term -> Term) -> Term -> Term
- Swarm.Language.Typecheck: getTypeErrLocation :: TypeErr -> Maybe Location
- Swarm.Language.Typecheck: instance Swarm.Language.Typecheck.HasBindings Swarm.Language.Types.UModule
- Swarm.Language.Types: BRobot :: BaseTy
- Swarm.Language.Types: Module :: s -> Ctx t -> Module s t
- Swarm.Language.Types: [moduleCtx] :: Module s t -> Ctx t
- Swarm.Language.Types: [moduleTy] :: Module s t -> s
- Swarm.Language.Types: data Module s t
- Swarm.Language.Types: instance (Data.Aeson.Types.FromJSON.FromJSON s, Data.Aeson.Types.FromJSON.FromJSON t) => Data.Aeson.Types.FromJSON.FromJSON (Swarm.Language.Types.Module s t)
- Swarm.Language.Types: instance (Data.Aeson.Types.ToJSON.ToJSON t, Data.Aeson.Types.ToJSON.ToJSON s) => Data.Aeson.Types.ToJSON.ToJSON (Swarm.Language.Types.Module s t)
- Swarm.Language.Types: instance (Data.Data.Data s, Data.Data.Data t) => Data.Data.Data (Swarm.Language.Types.Module s t)
- Swarm.Language.Types: instance (GHC.Classes.Eq s, GHC.Classes.Eq t) => GHC.Classes.Eq (Swarm.Language.Types.Module s t)
- Swarm.Language.Types: instance (GHC.Show.Show s, GHC.Show.Show t) => GHC.Show.Show (Swarm.Language.Types.Module s t)
- Swarm.Language.Types: instance GHC.Base.Functor (Swarm.Language.Types.Module s)
- Swarm.Language.Types: instance GHC.Generics.Generic (Swarm.Language.Types.Module s t)
- Swarm.Language.Types: pattern TyRobot :: Type
- Swarm.Language.Types: pattern UTyRobot :: UType
- Swarm.Language.Types: trivMod :: s -> Module s t
- Swarm.Language.Types: type TModule = Module Polytype Polytype
- Swarm.Language.Types: type UModule = Module UType UPolytype
- Swarm.TUI.Model: InstalledEntry :: Entity -> InventoryListEntry
- Swarm.TUI.Model: _InstalledEntry :: Prism' InventoryListEntry Entity
- Swarm.TUI.Model: accumulatedTime :: Lens' UIState TimeSpec
- Swarm.TUI.Model: appData :: Lens' UIState (Map Text Text)
- Swarm.TUI.Model: data ButtonSelection
- Swarm.TUI.Model: data UIState
- Swarm.TUI.Model: defaultPrompt :: REPLPrompt
- Swarm.TUI.Model: frameCount :: Lens' UIState Int
- Swarm.TUI.Model: frameTickCount :: Lens' UIState Int
- Swarm.TUI.Model: initAppState :: AppOpts -> ExceptT Text IO AppState
- Swarm.TUI.Model: initFocusRing :: FocusRing Name
- Swarm.TUI.Model: initLgTicksPerSecond :: Int
- Swarm.TUI.Model: initREPLState :: REPLHistory -> REPLState
- Swarm.TUI.Model: initUIState :: Bool -> Bool -> ExceptT Text IO UIState
- Swarm.TUI.Model: instance GHC.Classes.Eq Swarm.TUI.Model.InventoryListEntry
- Swarm.TUI.Model: instance GHC.Classes.Eq Swarm.TUI.Model.MainMenuEntry
- Swarm.TUI.Model: instance GHC.Classes.Eq Swarm.TUI.Model.ModalType
- Swarm.TUI.Model: instance GHC.Classes.Eq Swarm.TUI.Model.Name
- Swarm.TUI.Model: instance GHC.Classes.Eq Swarm.TUI.Model.REPLHistItem
- Swarm.TUI.Model: instance GHC.Classes.Eq Swarm.TUI.Model.TimeDir
- Swarm.TUI.Model: instance GHC.Classes.Ord Swarm.TUI.Model.MainMenuEntry
- Swarm.TUI.Model: instance GHC.Classes.Ord Swarm.TUI.Model.Name
- Swarm.TUI.Model: instance GHC.Classes.Ord Swarm.TUI.Model.REPLHistItem
- Swarm.TUI.Model: instance GHC.Classes.Ord Swarm.TUI.Model.TimeDir
- Swarm.TUI.Model: instance GHC.Enum.Bounded Swarm.TUI.Model.MainMenuEntry
- Swarm.TUI.Model: instance GHC.Enum.Enum Swarm.TUI.Model.MainMenuEntry
- Swarm.TUI.Model: instance GHC.Read.Read Swarm.TUI.Model.MainMenuEntry
- Swarm.TUI.Model: instance GHC.Read.Read Swarm.TUI.Model.Name
- Swarm.TUI.Model: instance GHC.Read.Read Swarm.TUI.Model.REPLHistItem
- Swarm.TUI.Model: instance GHC.Show.Show Swarm.TUI.Model.MainMenuEntry
- Swarm.TUI.Model: instance GHC.Show.Show Swarm.TUI.Model.ModalType
- Swarm.TUI.Model: instance GHC.Show.Show Swarm.TUI.Model.Name
- Swarm.TUI.Model: instance GHC.Show.Show Swarm.TUI.Model.REPLHistItem
- Swarm.TUI.Model: instance GHC.Show.Show Swarm.TUI.Model.TimeDir
- Swarm.TUI.Model: lastEntry :: Text -> REPLHistory -> Maybe Text
- Swarm.TUI.Model: lastFrameTime :: Lens' UIState TimeSpec
- Swarm.TUI.Model: lastInfoTime :: Lens' UIState TimeSpec
- Swarm.TUI.Model: lgTicksPerSecond :: Lens' UIState Int
- Swarm.TUI.Model: restartGame :: (MonadIO m, MonadState AppState m) => Seed -> ScenarioInfoPair -> m ()
- Swarm.TUI.Model: scenarioRef :: Lens' UIState (Maybe ScenarioInfoPair)
- Swarm.TUI.Model: scenarioToAppState :: (MonadIO m, MonadState AppState m) => ScenarioInfoPair -> Maybe Seed -> Maybe CodeToRun -> m ()
- Swarm.TUI.Model: startGame :: (MonadIO m, MonadState AppState m) => ScenarioInfoPair -> Maybe CodeToRun -> m ()
- Swarm.TUI.Model: tickCount :: Lens' UIState Int
- Swarm.TUI.Model: uiCheatMode :: Lens' UIState Bool
- Swarm.TUI.Model: uiError :: Lens' UIState (Maybe Text)
- Swarm.TUI.Model: uiFPS :: Lens' UIState Double
- Swarm.TUI.Model: uiFocusRing :: Lens' UIState (FocusRing Name)
- Swarm.TUI.Model: uiGoal :: Lens' UIState (Maybe [Text])
- Swarm.TUI.Model: uiHideRobotsUntil :: Lens' UIState TimeSpec
- Swarm.TUI.Model: uiInventory :: Lens' UIState (Maybe (Int, List Name InventoryListEntry))
- Swarm.TUI.Model: uiInventoryShouldUpdate :: Lens' UIState Bool
- Swarm.TUI.Model: uiInventorySort :: Lens' UIState InventorySortOptions
- Swarm.TUI.Model: uiMenu :: Lens' UIState Menu
- Swarm.TUI.Model: uiModal :: Lens' UIState (Maybe Modal)
- Swarm.TUI.Model: uiMoreInfoBot :: Lens' UIState Bool
- Swarm.TUI.Model: uiMoreInfoTop :: Lens' UIState Bool
- Swarm.TUI.Model: uiPlaying :: Lens' UIState Bool
- Swarm.TUI.Model: uiREPL :: Lens' UIState REPLState
- Swarm.TUI.Model: uiScrollToEnd :: Lens' UIState Bool
- Swarm.TUI.Model: uiShowFPS :: Lens' UIState Bool
- Swarm.TUI.Model: uiShowRobots :: Getter UIState Bool
- Swarm.TUI.Model: uiShowZero :: Lens' UIState Bool
- Swarm.TUI.Model: uiTPF :: Lens' UIState Double
- Swarm.TUI.Model: uiWorldCursor :: Lens' UIState (Maybe Coords)
- Swarm.Util: getSwarmDataPath :: Bool -> IO FilePath
- Swarm.Util: instance Data.Aeson.Types.FromJSON.FromJSON (Linear.V2.V2 GHC.Int.Int64)
- Swarm.Util: instance Data.Aeson.Types.FromJSON.FromJSONKey (Linear.V2.V2 GHC.Int.Int64)
- Swarm.Util: instance Data.Aeson.Types.ToJSON.ToJSON (Linear.V2.V2 GHC.Int.Int64)
- Swarm.Util: instance Data.Aeson.Types.ToJSON.ToJSONKey (Linear.V2.V2 GHC.Int.Int64)
+ Data.BoolExpr: (/\) :: Boolean f => f a -> f a -> f a
+ Data.BoolExpr: (\/) :: Boolean f => f a -> f a -> f a
+ Data.BoolExpr: BAnd :: BoolExpr a -> BoolExpr a -> BoolExpr a
+ Data.BoolExpr: BConst :: Signed a -> BoolExpr a
+ Data.BoolExpr: BFalse :: BoolExpr a
+ Data.BoolExpr: BNot :: BoolExpr a -> BoolExpr a
+ Data.BoolExpr: BOr :: BoolExpr a -> BoolExpr a -> BoolExpr a
+ Data.BoolExpr: BTrue :: BoolExpr a
+ Data.BoolExpr: CNF :: Conj (Disj (Signed a)) -> CNF a
+ Data.BoolExpr: Conj :: [a] -> Conj a
+ Data.BoolExpr: DNF :: Disj (Conj (Signed a)) -> DNF a
+ Data.BoolExpr: Disj :: [a] -> Disj a
+ Data.BoolExpr: Eval :: ((a -> b) -> b) -> Eval b a
+ Data.BoolExpr: Negative :: a -> Signed a
+ Data.BoolExpr: Positive :: a -> Signed a
+ Data.BoolExpr: [runEval] :: Eval b a -> (a -> b) -> b
+ Data.BoolExpr: [unCNF] :: CNF a -> Conj (Disj (Signed a))
+ Data.BoolExpr: [unConj] :: Conj a -> [a]
+ Data.BoolExpr: [unDNF] :: DNF a -> Disj (Conj (Signed a))
+ Data.BoolExpr: [unDisj] :: Disj a -> [a]
+ Data.BoolExpr: bConst :: Boolean f => Signed a -> f a
+ Data.BoolExpr: bFalse :: Boolean f => f a
+ Data.BoolExpr: bNot :: Boolean f => f a -> f a
+ Data.BoolExpr: bTrue :: Boolean f => f a
+ Data.BoolExpr: boolTreeToCNF :: BoolExpr a -> CNF a
+ Data.BoolExpr: boolTreeToDNF :: BoolExpr a -> DNF a
+ Data.BoolExpr: class Boolean f
+ Data.BoolExpr: constants :: BoolExpr a -> [Signed a]
+ Data.BoolExpr: data BoolExpr a
+ Data.BoolExpr: data Signed a
+ Data.BoolExpr: dualize :: Boolean f => BoolExpr a -> f a
+ Data.BoolExpr: evalBoolExpr :: (a -> Bool) -> BoolExpr a -> Bool
+ Data.BoolExpr: evalSigned :: (a -> Bool) -> Signed a -> Bool
+ Data.BoolExpr: fromBoolExpr :: Boolean f => BoolExpr a -> f a
+ Data.BoolExpr: infix 9 \/
+ Data.BoolExpr: instance (b GHC.Types.~ GHC.Types.Bool) => Data.BoolExpr.Boolean (Data.BoolExpr.Eval b)
+ Data.BoolExpr: instance Data.Aeson.Types.ToJSON.ToJSON a => Data.Aeson.Types.ToJSON.ToJSON (Data.BoolExpr.BoolExpr a)
+ Data.BoolExpr: instance Data.Aeson.Types.ToJSON.ToJSON a => Data.Aeson.Types.ToJSON.ToJSON (Data.BoolExpr.CNF a)
+ Data.BoolExpr: instance Data.Aeson.Types.ToJSON.ToJSON a => Data.Aeson.Types.ToJSON.ToJSON (Data.BoolExpr.Conj a)
+ Data.BoolExpr: instance Data.Aeson.Types.ToJSON.ToJSON a => Data.Aeson.Types.ToJSON.ToJSON (Data.BoolExpr.DNF a)
+ Data.BoolExpr: instance Data.Aeson.Types.ToJSON.ToJSON a => Data.Aeson.Types.ToJSON.ToJSON (Data.BoolExpr.Disj a)
+ Data.BoolExpr: instance Data.Aeson.Types.ToJSON.ToJSON a => Data.Aeson.Types.ToJSON.ToJSON (Data.BoolExpr.Signed a)
+ Data.BoolExpr: instance Data.BoolExpr.Boolean Data.BoolExpr.BoolExpr
+ Data.BoolExpr: instance Data.BoolExpr.Boolean Data.BoolExpr.CNF
+ Data.BoolExpr: instance Data.BoolExpr.Boolean Data.BoolExpr.DNF
+ Data.BoolExpr: instance Data.Foldable.Foldable Data.BoolExpr.BoolExpr
+ Data.BoolExpr: instance Data.Foldable.Foldable Data.BoolExpr.Signed
+ Data.BoolExpr: instance Data.Traversable.Traversable Data.BoolExpr.BoolExpr
+ Data.BoolExpr: instance Data.Traversable.Traversable Data.BoolExpr.Signed
+ Data.BoolExpr: instance GHC.Base.Applicative Data.BoolExpr.Signed
+ Data.BoolExpr: instance GHC.Base.Functor Data.BoolExpr.BoolExpr
+ Data.BoolExpr: instance GHC.Base.Functor Data.BoolExpr.CNF
+ Data.BoolExpr: instance GHC.Base.Functor Data.BoolExpr.Conj
+ Data.BoolExpr: instance GHC.Base.Functor Data.BoolExpr.DNF
+ Data.BoolExpr: instance GHC.Base.Functor Data.BoolExpr.Disj
+ Data.BoolExpr: instance GHC.Base.Functor Data.BoolExpr.Signed
+ Data.BoolExpr: instance GHC.Base.Monad Data.BoolExpr.Signed
+ Data.BoolExpr: instance GHC.Base.Monoid (Data.BoolExpr.CNF a)
+ Data.BoolExpr: instance GHC.Base.Monoid (Data.BoolExpr.Conj a)
+ Data.BoolExpr: instance GHC.Base.Monoid (Data.BoolExpr.DNF a)
+ Data.BoolExpr: instance GHC.Base.Monoid (Data.BoolExpr.Disj a)
+ Data.BoolExpr: instance GHC.Base.Semigroup (Data.BoolExpr.CNF a)
+ Data.BoolExpr: instance GHC.Base.Semigroup (Data.BoolExpr.Conj a)
+ Data.BoolExpr: instance GHC.Base.Semigroup (Data.BoolExpr.DNF a)
+ Data.BoolExpr: instance GHC.Base.Semigroup (Data.BoolExpr.Disj a)
+ Data.BoolExpr: instance GHC.Classes.Eq a => GHC.Classes.Eq (Data.BoolExpr.BoolExpr a)
+ Data.BoolExpr: instance GHC.Classes.Eq a => GHC.Classes.Eq (Data.BoolExpr.Signed a)
+ Data.BoolExpr: instance GHC.Classes.Ord a => GHC.Classes.Ord (Data.BoolExpr.BoolExpr a)
+ Data.BoolExpr: instance GHC.Classes.Ord a => GHC.Classes.Ord (Data.BoolExpr.Signed a)
+ Data.BoolExpr: instance GHC.Generics.Generic (Data.BoolExpr.BoolExpr a)
+ Data.BoolExpr: instance GHC.Generics.Generic (Data.BoolExpr.CNF a)
+ Data.BoolExpr: instance GHC.Generics.Generic (Data.BoolExpr.Conj a)
+ Data.BoolExpr: instance GHC.Generics.Generic (Data.BoolExpr.DNF a)
+ Data.BoolExpr: instance GHC.Generics.Generic (Data.BoolExpr.Disj a)
+ Data.BoolExpr: instance GHC.Generics.Generic (Data.BoolExpr.Signed a)
+ Data.BoolExpr: instance GHC.Read.Read a => GHC.Read.Read (Data.BoolExpr.Signed a)
+ Data.BoolExpr: instance GHC.Show.Show a => GHC.Show.Show (Data.BoolExpr.BoolExpr a)
+ Data.BoolExpr: instance GHC.Show.Show a => GHC.Show.Show (Data.BoolExpr.CNF a)
+ Data.BoolExpr: instance GHC.Show.Show a => GHC.Show.Show (Data.BoolExpr.Conj a)
+ Data.BoolExpr: instance GHC.Show.Show a => GHC.Show.Show (Data.BoolExpr.DNF a)
+ Data.BoolExpr: instance GHC.Show.Show a => GHC.Show.Show (Data.BoolExpr.Disj a)
+ Data.BoolExpr: instance GHC.Show.Show a => GHC.Show.Show (Data.BoolExpr.Signed a)
+ Data.BoolExpr: negateConstant :: Boolean f => Signed a -> f a
+ Data.BoolExpr: negateSigned :: Signed a -> Signed a
+ Data.BoolExpr: newtype CNF a
+ Data.BoolExpr: newtype Conj a
+ Data.BoolExpr: newtype DNF a
+ Data.BoolExpr: newtype Disj a
+ Data.BoolExpr: newtype Eval b a
+ Data.BoolExpr: pushNotInwards :: Boolean f => BoolExpr a -> f a
+ Data.BoolExpr: reduceBoolExpr :: BoolExpr Bool -> Bool
+ Data.BoolExpr: runEvalId :: Eval a a -> a
+ Data.BoolExpr.Simplify: cannotBeTrue :: Ord a => BoolExpr a -> Bool
+ Data.BoolExpr.Simplify: replace :: Ord a => Map a Bool -> BoolExpr a -> BoolExpr a
+ Swarm.Game.CESK: AddEntity :: Count -> Entity -> RobotUpdate
+ Swarm.Game.CESK: FDiscardEnv :: Frame
+ Swarm.Game.CESK: FMeetAll :: Value -> [Int] -> Frame
+ Swarm.Game.CESK: LearnEntity :: Entity -> RobotUpdate
+ Swarm.Game.CESK: ReplaceEntity :: Location -> e -> Maybe e -> WorldUpdate e
+ Swarm.Game.CESK: [newEntity] :: WorldUpdate e -> Maybe e
+ Swarm.Game.CESK: [originalEntity] :: WorldUpdate e -> e
+ Swarm.Game.CESK: [updatedLoc] :: WorldUpdate e -> Location
+ Swarm.Game.CESK: data RobotUpdate
+ Swarm.Game.CESK: data WorldUpdate e
+ Swarm.Game.CESK: instance GHC.Classes.Ord Swarm.Game.CESK.RobotUpdate
+ Swarm.Game.CESK: instance GHC.Generics.Generic Swarm.Game.CESK.RobotUpdate
+ Swarm.Game.CESK: instance Swarm.Language.Pretty.PrettyPrec Swarm.Game.CESK.CESK
+ Swarm.Game.CESK: type Addr = Int
+ Swarm.Game.Entity: entitiesByCapability :: Inventory -> Map Capability (NonEmpty Entity)
+ Swarm.Game.Entity: extantElemsWithCapability :: Capability -> Inventory -> [Entity]
+ Swarm.Game.Exception: FixByEquip :: IncapableFix
+ Swarm.Game.Log: Critical :: ErrorLevel
+ Swarm.Game.Log: Debug :: ErrorLevel
+ Swarm.Game.Log: Error :: ErrorLevel
+ Swarm.Game.Log: ErrorTrace :: ErrorLevel -> LogSource
+ Swarm.Game.Log: LogEntry :: Integer -> LogSource -> Text -> Int -> Location -> Text -> LogEntry
+ Swarm.Game.Log: Logged :: LogSource
+ Swarm.Game.Log: Said :: LogSource
+ Swarm.Game.Log: Warning :: ErrorLevel
+ Swarm.Game.Log: [_leLocation] :: LogEntry -> Location
+ Swarm.Game.Log: [_leRobotID] :: LogEntry -> Int
+ Swarm.Game.Log: [_leRobotName] :: LogEntry -> Text
+ Swarm.Game.Log: [_leSource] :: LogEntry -> LogSource
+ Swarm.Game.Log: [_leText] :: LogEntry -> Text
+ Swarm.Game.Log: [_leTime] :: LogEntry -> Integer
+ Swarm.Game.Log: data ErrorLevel
+ Swarm.Game.Log: data LogEntry
+ Swarm.Game.Log: data LogSource
+ Swarm.Game.Log: instance Data.Aeson.Types.FromJSON.FromJSON Swarm.Game.Log.ErrorLevel
+ Swarm.Game.Log: instance Data.Aeson.Types.FromJSON.FromJSON Swarm.Game.Log.LogEntry
+ Swarm.Game.Log: instance Data.Aeson.Types.FromJSON.FromJSON Swarm.Game.Log.LogSource
+ Swarm.Game.Log: instance Data.Aeson.Types.ToJSON.ToJSON Swarm.Game.Log.ErrorLevel
+ Swarm.Game.Log: instance Data.Aeson.Types.ToJSON.ToJSON Swarm.Game.Log.LogEntry
+ Swarm.Game.Log: instance Data.Aeson.Types.ToJSON.ToJSON Swarm.Game.Log.LogSource
+ Swarm.Game.Log: instance GHC.Classes.Eq Swarm.Game.Log.ErrorLevel
+ Swarm.Game.Log: instance GHC.Classes.Eq Swarm.Game.Log.LogEntry
+ Swarm.Game.Log: instance GHC.Classes.Eq Swarm.Game.Log.LogSource
+ Swarm.Game.Log: instance GHC.Classes.Ord Swarm.Game.Log.ErrorLevel
+ Swarm.Game.Log: instance GHC.Classes.Ord Swarm.Game.Log.LogEntry
+ Swarm.Game.Log: instance GHC.Classes.Ord Swarm.Game.Log.LogSource
+ Swarm.Game.Log: instance GHC.Generics.Generic Swarm.Game.Log.ErrorLevel
+ Swarm.Game.Log: instance GHC.Generics.Generic Swarm.Game.Log.LogEntry
+ Swarm.Game.Log: instance GHC.Generics.Generic Swarm.Game.Log.LogSource
+ Swarm.Game.Log: instance GHC.Show.Show Swarm.Game.Log.ErrorLevel
+ Swarm.Game.Log: instance GHC.Show.Show Swarm.Game.Log.LogEntry
+ Swarm.Game.Log: instance GHC.Show.Show Swarm.Game.Log.LogSource
+ Swarm.Game.Log: leLocation :: Lens' LogEntry Location
+ Swarm.Game.Log: leRobotID :: Lens' LogEntry Int
+ Swarm.Game.Log: leRobotName :: Lens' LogEntry Text
+ Swarm.Game.Log: leSource :: Lens' LogEntry LogSource
+ Swarm.Game.Log: leText :: Lens' LogEntry Text
+ Swarm.Game.Log: leTime :: Lens' LogEntry Integer
+ Swarm.Game.Robot: AddEntity :: Count -> Entity -> RobotUpdate
+ Swarm.Game.Robot: LearnEntity :: Entity -> RobotUpdate
+ Swarm.Game.Robot: data RobotUpdate
+ Swarm.Game.Robot: equippedDevices :: Lens' Robot Inventory
+ Swarm.Game.Robot: trobotContext :: Lens' TRobot RobotContext
+ Swarm.Game.Scenario: data PCell e
+ Swarm.Game.Scenario: data PWorldDescription e
+ Swarm.Game.Scenario: scenarioAttrs :: Lens' Scenario [CustomAttr]
+ Swarm.Game.Scenario: type Cell = PCell Entity
+ Swarm.Game.Scenario: type WorldDescription = PWorldDescription Entity
+ Swarm.Game.Scenario.Cell: Cell :: TerrainType -> Maybe e -> [IndexedTRobot] -> PCell e
+ Swarm.Game.Scenario.Cell: [cellEntity] :: PCell e -> Maybe e
+ Swarm.Game.Scenario.Cell: [cellRobots] :: PCell e -> [IndexedTRobot]
+ Swarm.Game.Scenario.Cell: [cellTerrain] :: PCell e -> TerrainType
+ Swarm.Game.Scenario.Cell: data PCell e
+ Swarm.Game.Scenario.Cell: instance GHC.Classes.Eq e => GHC.Classes.Eq (Swarm.Game.Scenario.Cell.PCell e)
+ Swarm.Game.Scenario.Cell: instance GHC.Show.Show e => GHC.Show.Show (Swarm.Game.Scenario.Cell.PCell e)
+ Swarm.Game.Scenario.Cell: instance Swarm.Util.Yaml.FromJSONE (Swarm.Game.Entity.EntityMap, Swarm.Game.Scenario.RobotLookup.RobotMap) Swarm.Game.Scenario.Cell.Cell
+ Swarm.Game.Scenario.Cell: type Cell = PCell Entity
+ Swarm.Game.Scenario.Objective: CompletionBuckets :: [Objective] -> [Objective] -> [Objective] -> CompletionBuckets
+ Swarm.Game.Scenario.Objective: Objective :: [Text] -> Maybe Text -> ProcessedTerm -> Maybe ObjectiveLabel -> Bool -> Maybe PrerequisiteConfig -> Bool -> Maybe AchievementInfo -> Objective
+ Swarm.Game.Scenario.Objective: ObjectiveCompletion :: CompletionBuckets -> Set ObjectiveLabel -> ObjectiveCompletion
+ Swarm.Game.Scenario.Objective: PrerequisiteConfig :: Bool -> Prerequisite ObjectiveLabel -> PrerequisiteConfig
+ Swarm.Game.Scenario.Objective: [_objectiveAchievement] :: Objective -> Maybe AchievementInfo
+ Swarm.Game.Scenario.Objective: [_objectiveCondition] :: Objective -> ProcessedTerm
+ Swarm.Game.Scenario.Objective: [_objectiveGoal] :: Objective -> [Text]
+ Swarm.Game.Scenario.Objective: [_objectiveHidden] :: Objective -> Bool
+ Swarm.Game.Scenario.Objective: [_objectiveId] :: Objective -> Maybe ObjectiveLabel
+ Swarm.Game.Scenario.Objective: [_objectiveOptional] :: Objective -> Bool
+ Swarm.Game.Scenario.Objective: [_objectivePrerequisite] :: Objective -> Maybe PrerequisiteConfig
+ Swarm.Game.Scenario.Objective: [_objectiveTeaser] :: Objective -> Maybe Text
+ Swarm.Game.Scenario.Objective: [completedIDs] :: ObjectiveCompletion -> Set ObjectiveLabel
+ Swarm.Game.Scenario.Objective: [completed] :: CompletionBuckets -> [Objective]
+ Swarm.Game.Scenario.Objective: [completionBuckets] :: ObjectiveCompletion -> CompletionBuckets
+ Swarm.Game.Scenario.Objective: [incomplete] :: CompletionBuckets -> [Objective]
+ Swarm.Game.Scenario.Objective: [logic] :: PrerequisiteConfig -> Prerequisite ObjectiveLabel
+ Swarm.Game.Scenario.Objective: [previewable] :: PrerequisiteConfig -> Bool
+ Swarm.Game.Scenario.Objective: [unwinnable] :: CompletionBuckets -> [Objective]
+ Swarm.Game.Scenario.Objective: addCompleted :: Objective -> ObjectiveCompletion -> ObjectiveCompletion
+ Swarm.Game.Scenario.Objective: addIncomplete :: Objective -> ObjectiveCompletion -> ObjectiveCompletion
+ Swarm.Game.Scenario.Objective: addUnwinnable :: Objective -> ObjectiveCompletion -> ObjectiveCompletion
+ Swarm.Game.Scenario.Objective: data CompletionBuckets
+ Swarm.Game.Scenario.Objective: data Objective
+ Swarm.Game.Scenario.Objective: data ObjectiveCompletion
+ Swarm.Game.Scenario.Objective: data PrerequisiteConfig
+ Swarm.Game.Scenario.Objective: extractIncomplete :: ObjectiveCompletion -> (ObjectiveCompletion, [Objective])
+ Swarm.Game.Scenario.Objective: instance Data.Aeson.Types.FromJSON.FromJSON Swarm.Game.Scenario.Objective.CompletionBuckets
+ Swarm.Game.Scenario.Objective: instance Data.Aeson.Types.FromJSON.FromJSON Swarm.Game.Scenario.Objective.Objective
+ Swarm.Game.Scenario.Objective: instance Data.Aeson.Types.FromJSON.FromJSON Swarm.Game.Scenario.Objective.ObjectiveCompletion
+ Swarm.Game.Scenario.Objective: instance Data.Aeson.Types.FromJSON.FromJSON Swarm.Game.Scenario.Objective.PrerequisiteConfig
+ Swarm.Game.Scenario.Objective: instance Data.Aeson.Types.ToJSON.ToJSON Swarm.Game.Scenario.Objective.CompletionBuckets
+ Swarm.Game.Scenario.Objective: instance Data.Aeson.Types.ToJSON.ToJSON Swarm.Game.Scenario.Objective.Objective
+ Swarm.Game.Scenario.Objective: instance Data.Aeson.Types.ToJSON.ToJSON Swarm.Game.Scenario.Objective.ObjectiveCompletion
+ Swarm.Game.Scenario.Objective: instance Data.Aeson.Types.ToJSON.ToJSON Swarm.Game.Scenario.Objective.PrerequisiteConfig
+ Swarm.Game.Scenario.Objective: instance GHC.Classes.Eq Swarm.Game.Scenario.Objective.Objective
+ Swarm.Game.Scenario.Objective: instance GHC.Classes.Eq Swarm.Game.Scenario.Objective.PrerequisiteConfig
+ Swarm.Game.Scenario.Objective: instance GHC.Generics.Generic Swarm.Game.Scenario.Objective.CompletionBuckets
+ Swarm.Game.Scenario.Objective: instance GHC.Generics.Generic Swarm.Game.Scenario.Objective.Objective
+ Swarm.Game.Scenario.Objective: instance GHC.Generics.Generic Swarm.Game.Scenario.Objective.ObjectiveCompletion
+ Swarm.Game.Scenario.Objective: instance GHC.Generics.Generic Swarm.Game.Scenario.Objective.PrerequisiteConfig
+ Swarm.Game.Scenario.Objective: instance GHC.Show.Show Swarm.Game.Scenario.Objective.CompletionBuckets
+ Swarm.Game.Scenario.Objective: instance GHC.Show.Show Swarm.Game.Scenario.Objective.Objective
+ Swarm.Game.Scenario.Objective: instance GHC.Show.Show Swarm.Game.Scenario.Objective.ObjectiveCompletion
+ Swarm.Game.Scenario.Objective: instance GHC.Show.Show Swarm.Game.Scenario.Objective.PrerequisiteConfig
+ Swarm.Game.Scenario.Objective: listAllObjectives :: CompletionBuckets -> [Objective]
+ Swarm.Game.Scenario.Objective: objectiveAchievement :: Lens' Objective (Maybe AchievementInfo)
+ Swarm.Game.Scenario.Objective: objectiveCondition :: Lens' Objective ProcessedTerm
+ Swarm.Game.Scenario.Objective: objectiveGoal :: Lens' Objective [Text]
+ Swarm.Game.Scenario.Objective: objectiveHidden :: Lens' Objective Bool
+ Swarm.Game.Scenario.Objective: objectiveId :: Lens' Objective (Maybe Text)
+ Swarm.Game.Scenario.Objective: objectiveOptional :: Lens' Objective Bool
+ Swarm.Game.Scenario.Objective: objectivePrerequisite :: Lens' Objective (Maybe PrerequisiteConfig)
+ Swarm.Game.Scenario.Objective: objectiveTeaser :: Lens' Objective (Maybe Text)
+ Swarm.Game.Scenario.Objective: setIncomplete :: ([Objective] -> [Objective]) -> ObjectiveCompletion -> ObjectiveCompletion
+ Swarm.Game.Scenario.Objective.Graph: GraphInfo :: Graph -> Bool -> [SCC Objective] -> [ObjectiveId] -> GraphInfo
+ Swarm.Game.Scenario.Objective.Graph: Label :: Signed ObjectiveLabel -> ObjectiveId
+ Swarm.Game.Scenario.Objective.Graph: Ordinal :: Int -> ObjectiveId
+ Swarm.Game.Scenario.Objective.Graph: [actualGraph] :: GraphInfo -> Graph
+ Swarm.Game.Scenario.Objective.Graph: [isAcyclic] :: GraphInfo -> Bool
+ Swarm.Game.Scenario.Objective.Graph: [nodeIDs] :: GraphInfo -> [ObjectiveId]
+ Swarm.Game.Scenario.Objective.Graph: [sccInfo] :: GraphInfo -> [SCC Objective]
+ Swarm.Game.Scenario.Objective.Graph: assignIds :: [Objective] -> Map ObjectiveId Objective
+ Swarm.Game.Scenario.Objective.Graph: data GraphInfo
+ Swarm.Game.Scenario.Objective.Graph: data ObjectiveId
+ Swarm.Game.Scenario.Objective.Graph: getConstFromSigned :: Signed a -> a
+ Swarm.Game.Scenario.Objective.Graph: getNegatedIds :: [Objective] -> Map ObjectiveLabel Objective
+ Swarm.Game.Scenario.Objective.Graph: getObjectivesById :: [Objective] -> Map ObjectiveLabel Objective
+ Swarm.Game.Scenario.Objective.Graph: instance Data.Aeson.Types.ToJSON.ToJSON (Data.Graph.SCC Swarm.Game.Scenario.Objective.Objective)
+ Swarm.Game.Scenario.Objective.Graph: instance Data.Aeson.Types.ToJSON.ToJSON Data.Graph.Graph
+ Swarm.Game.Scenario.Objective.Graph: instance Data.Aeson.Types.ToJSON.ToJSON Swarm.Game.Scenario.Objective.Graph.GraphInfo
+ Swarm.Game.Scenario.Objective.Graph: instance Data.Aeson.Types.ToJSON.ToJSON Swarm.Game.Scenario.Objective.Graph.ObjectiveId
+ Swarm.Game.Scenario.Objective.Graph: instance GHC.Classes.Eq Swarm.Game.Scenario.Objective.Graph.ObjectiveId
+ Swarm.Game.Scenario.Objective.Graph: instance GHC.Classes.Ord Swarm.Game.Scenario.Objective.Graph.ObjectiveId
+ Swarm.Game.Scenario.Objective.Graph: instance GHC.Generics.Generic Swarm.Game.Scenario.Objective.Graph.GraphInfo
+ Swarm.Game.Scenario.Objective.Graph: instance GHC.Generics.Generic Swarm.Game.Scenario.Objective.Graph.ObjectiveId
+ Swarm.Game.Scenario.Objective.Graph: instance GHC.Show.Show Swarm.Game.Scenario.Objective.Graph.GraphInfo
+ Swarm.Game.Scenario.Objective.Graph: instance GHC.Show.Show Swarm.Game.Scenario.Objective.Graph.ObjectiveId
+ Swarm.Game.Scenario.Objective.Graph: isAcyclicGraph :: [SCC Objective] -> Bool
+ Swarm.Game.Scenario.Objective.Graph: makeGraph :: Edges -> Graph
+ Swarm.Game.Scenario.Objective.Graph: makeGraphEdges :: [Objective] -> Edges
+ Swarm.Game.Scenario.Objective.Graph: makeGraphInfo :: ObjectiveCompletion -> GraphInfo
+ Swarm.Game.Scenario.Objective.Graph: type Edges = [(Objective, ObjectiveId, [ObjectiveId])]
+ Swarm.Game.Scenario.Objective.Logic: And :: NonEmpty (Prerequisite a) -> Prerequisite a
+ Swarm.Game.Scenario.Objective.Logic: Id :: a -> Prerequisite a
+ Swarm.Game.Scenario.Objective.Logic: Not :: Prerequisite a -> Prerequisite a
+ Swarm.Game.Scenario.Objective.Logic: Or :: NonEmpty (Prerequisite a) -> Prerequisite a
+ Swarm.Game.Scenario.Objective.Logic: data Prerequisite a
+ Swarm.Game.Scenario.Objective.Logic: instance Data.Aeson.Types.FromJSON.FromJSON (Swarm.Game.Scenario.Objective.Logic.Prerequisite Swarm.Game.Scenario.Objective.Logic.ObjectiveLabel)
+ Swarm.Game.Scenario.Objective.Logic: instance Data.Aeson.Types.ToJSON.ToJSON (Swarm.Game.Scenario.Objective.Logic.Prerequisite Swarm.Game.Scenario.Objective.Logic.ObjectiveLabel)
+ Swarm.Game.Scenario.Objective.Logic: instance Data.Foldable.Foldable Swarm.Game.Scenario.Objective.Logic.Prerequisite
+ Swarm.Game.Scenario.Objective.Logic: instance GHC.Base.Functor Swarm.Game.Scenario.Objective.Logic.Prerequisite
+ Swarm.Game.Scenario.Objective.Logic: instance GHC.Classes.Eq a => GHC.Classes.Eq (Swarm.Game.Scenario.Objective.Logic.Prerequisite a)
+ Swarm.Game.Scenario.Objective.Logic: instance GHC.Generics.Generic (Swarm.Game.Scenario.Objective.Logic.Prerequisite a)
+ Swarm.Game.Scenario.Objective.Logic: instance GHC.Show.Show a => GHC.Show.Show (Swarm.Game.Scenario.Objective.Logic.Prerequisite a)
+ Swarm.Game.Scenario.Objective.Logic: prerequisiteOptions :: Options
+ Swarm.Game.Scenario.Objective.Logic: toBoolExpr :: Prerequisite a -> BoolExpr a
+ Swarm.Game.Scenario.Objective.Logic: type ObjectiveLabel = Text
+ Swarm.Game.Scenario.Objective.Presentation.Model: Active :: GoalStatus
+ Swarm.Game.Scenario.Objective.Presentation.Model: Completed :: GoalStatus
+ Swarm.Game.Scenario.Objective.Presentation.Model: Failed :: GoalStatus
+ Swarm.Game.Scenario.Objective.Presentation.Model: Goal :: GoalStatus -> Objective -> GoalEntry
+ Swarm.Game.Scenario.Objective.Presentation.Model: GoalDisplay :: GoalTracking -> List Name GoalEntry -> FocusRing Name -> GoalDisplay
+ Swarm.Game.Scenario.Objective.Presentation.Model: GoalTracking :: [Announcement] -> CategorizedGoals -> GoalTracking
+ Swarm.Game.Scenario.Objective.Presentation.Model: Header :: GoalStatus -> GoalEntry
+ Swarm.Game.Scenario.Objective.Presentation.Model: ObjectiveCompleted :: Objective -> Announcement
+ Swarm.Game.Scenario.Objective.Presentation.Model: Upcoming :: GoalStatus
+ Swarm.Game.Scenario.Objective.Presentation.Model: [_focus] :: GoalDisplay -> FocusRing Name
+ Swarm.Game.Scenario.Objective.Presentation.Model: [_goalsContent] :: GoalDisplay -> GoalTracking
+ Swarm.Game.Scenario.Objective.Presentation.Model: [_listWidget] :: GoalDisplay -> List Name GoalEntry
+ Swarm.Game.Scenario.Objective.Presentation.Model: [announcements] :: GoalTracking -> [Announcement]
+ Swarm.Game.Scenario.Objective.Presentation.Model: [goals] :: GoalTracking -> CategorizedGoals
+ Swarm.Game.Scenario.Objective.Presentation.Model: constructGoalMap :: Bool -> ObjectiveCompletion -> CategorizedGoals
+ Swarm.Game.Scenario.Objective.Presentation.Model: data GoalDisplay
+ Swarm.Game.Scenario.Objective.Presentation.Model: data GoalEntry
+ Swarm.Game.Scenario.Objective.Presentation.Model: data GoalStatus
+ Swarm.Game.Scenario.Objective.Presentation.Model: data GoalTracking
+ Swarm.Game.Scenario.Objective.Presentation.Model: emptyGoalDisplay :: GoalDisplay
+ Swarm.Game.Scenario.Objective.Presentation.Model: focus :: Lens' GoalDisplay (FocusRing Name)
+ Swarm.Game.Scenario.Objective.Presentation.Model: goalsContent :: Lens' GoalDisplay GoalTracking
+ Swarm.Game.Scenario.Objective.Presentation.Model: hasAnythingToShow :: GoalTracking -> Bool
+ Swarm.Game.Scenario.Objective.Presentation.Model: hasMultipleGoals :: GoalTracking -> Bool
+ Swarm.Game.Scenario.Objective.Presentation.Model: instance Data.Aeson.Types.ToJSON.ToJSON Swarm.Game.Scenario.Objective.Presentation.Model.Announcement
+ Swarm.Game.Scenario.Objective.Presentation.Model: instance Data.Aeson.Types.ToJSON.ToJSON Swarm.Game.Scenario.Objective.Presentation.Model.GoalStatus
+ Swarm.Game.Scenario.Objective.Presentation.Model: instance Data.Aeson.Types.ToJSON.ToJSON Swarm.Game.Scenario.Objective.Presentation.Model.GoalTracking
+ Swarm.Game.Scenario.Objective.Presentation.Model: instance Data.Aeson.Types.ToJSON.ToJSONKey Swarm.Game.Scenario.Objective.Presentation.Model.GoalStatus
+ Swarm.Game.Scenario.Objective.Presentation.Model: instance GHC.Classes.Eq Swarm.Game.Scenario.Objective.Presentation.Model.GoalStatus
+ Swarm.Game.Scenario.Objective.Presentation.Model: instance GHC.Classes.Ord Swarm.Game.Scenario.Objective.Presentation.Model.GoalStatus
+ Swarm.Game.Scenario.Objective.Presentation.Model: instance GHC.Enum.Bounded Swarm.Game.Scenario.Objective.Presentation.Model.GoalStatus
+ Swarm.Game.Scenario.Objective.Presentation.Model: instance GHC.Enum.Enum Swarm.Game.Scenario.Objective.Presentation.Model.GoalStatus
+ Swarm.Game.Scenario.Objective.Presentation.Model: instance GHC.Generics.Generic Swarm.Game.Scenario.Objective.Presentation.Model.Announcement
+ Swarm.Game.Scenario.Objective.Presentation.Model: instance GHC.Generics.Generic Swarm.Game.Scenario.Objective.Presentation.Model.GoalStatus
+ Swarm.Game.Scenario.Objective.Presentation.Model: instance GHC.Generics.Generic Swarm.Game.Scenario.Objective.Presentation.Model.GoalTracking
+ Swarm.Game.Scenario.Objective.Presentation.Model: instance GHC.Show.Show Swarm.Game.Scenario.Objective.Presentation.Model.Announcement
+ Swarm.Game.Scenario.Objective.Presentation.Model: instance GHC.Show.Show Swarm.Game.Scenario.Objective.Presentation.Model.GoalStatus
+ Swarm.Game.Scenario.Objective.Presentation.Model: isHeader :: GoalEntry -> Bool
+ Swarm.Game.Scenario.Objective.Presentation.Model: listWidget :: Lens' GoalDisplay (List Name GoalEntry)
+ Swarm.Game.Scenario.Objective.Presentation.Model: newtype Announcement
+ Swarm.Game.Scenario.Objective.Presentation.Model: type CategorizedGoals = Map GoalStatus (NonEmpty Objective)
+ Swarm.Game.Scenario.Objective.Presentation.Render: drawGoalListItem :: Bool -> GoalEntry -> Widget Name
+ Swarm.Game.Scenario.Objective.Presentation.Render: getCompletionIcon :: Objective -> GoalStatus -> Widget Name
+ Swarm.Game.Scenario.Objective.Presentation.Render: makeListWidget :: GoalTracking -> List Name GoalEntry
+ Swarm.Game.Scenario.Objective.Presentation.Render: renderGoalsDisplay :: GoalDisplay -> Widget Name
+ Swarm.Game.Scenario.Objective.Presentation.Render: singleGoalDetails :: GoalEntry -> Widget Name
+ Swarm.Game.Scenario.Objective.Validation: validateObjectives :: MonadFail m => [Objective] -> m [Objective]
+ Swarm.Game.Scenario.Objective.WinCheck: PrereqSatisfaction :: Objective -> Set (Signed ObjectiveLabel) -> Bool -> PrereqSatisfaction
+ Swarm.Game.Scenario.Objective.WinCheck: [deps] :: PrereqSatisfaction -> Set (Signed ObjectiveLabel)
+ Swarm.Game.Scenario.Objective.WinCheck: [objective] :: PrereqSatisfaction -> Objective
+ Swarm.Game.Scenario.Objective.WinCheck: [prereqsSatisfied] :: PrereqSatisfaction -> Bool
+ Swarm.Game.Scenario.Objective.WinCheck: data PrereqSatisfaction
+ Swarm.Game.Scenario.Objective.WinCheck: didLose :: ObjectiveCompletion -> Bool
+ Swarm.Game.Scenario.Objective.WinCheck: didWin :: ObjectiveCompletion -> Bool
+ Swarm.Game.Scenario.Objective.WinCheck: getActiveObjectives :: ObjectiveCompletion -> [Objective]
+ Swarm.Game.Scenario.Objective.WinCheck: getDistinctConstants :: Ord a => Prerequisite a -> Set (Signed a)
+ Swarm.Game.Scenario.Objective.WinCheck: getSatisfaction :: ObjectiveCompletion -> [PrereqSatisfaction]
+ Swarm.Game.Scenario.Objective.WinCheck: instance Data.Aeson.Types.ToJSON.ToJSON Swarm.Game.Scenario.Objective.WinCheck.PrereqSatisfaction
+ Swarm.Game.Scenario.Objective.WinCheck: instance GHC.Generics.Generic Swarm.Game.Scenario.Objective.WinCheck.PrereqSatisfaction
+ Swarm.Game.Scenario.Objective.WinCheck: isPrereqsSatisfied :: ObjectiveCompletion -> Objective -> Bool
+ Swarm.Game.Scenario.Objective.WinCheck: isUnwinnable :: ObjectiveCompletion -> Objective -> Bool
+ Swarm.Game.Scenario.Objective.WinCheck: isUnwinnablePrereq :: Set ObjectiveLabel -> Prerequisite ObjectiveLabel -> Bool
+ Swarm.Game.Scenario.Objective.WinCheck: partitionActiveObjectives :: ObjectiveCompletion -> ([Objective], [Objective])
+ Swarm.Game.Scenario.RobotLookup: buildRobotMap :: [TRobot] -> RobotMap
+ Swarm.Game.Scenario.RobotLookup: getEntity :: Text -> ParserE EntityMap Entity
+ Swarm.Game.Scenario.RobotLookup: getRobot :: Text -> ParserE RobotMap IndexedTRobot
+ Swarm.Game.Scenario.RobotLookup: getThing :: String -> (Text -> m -> Maybe a) -> Text -> ParserE m a
+ Swarm.Game.Scenario.RobotLookup: type IndexedTRobot = (Int, TRobot)
+ Swarm.Game.Scenario.RobotLookup: type RobotMap = Map Text IndexedTRobot
+ Swarm.Game.Scenario.Style: Blink :: StyleFlag
+ Swarm.Game.Scenario.Style: Bold :: StyleFlag
+ Swarm.Game.Scenario.Style: CustomAttr :: String -> Maybe HexColor -> Maybe HexColor -> Maybe (Set StyleFlag) -> CustomAttr
+ Swarm.Game.Scenario.Style: Dim :: StyleFlag
+ Swarm.Game.Scenario.Style: HexColor :: Text -> HexColor
+ Swarm.Game.Scenario.Style: Italic :: StyleFlag
+ Swarm.Game.Scenario.Style: ReverseVideo :: StyleFlag
+ Swarm.Game.Scenario.Style: Standout :: StyleFlag
+ Swarm.Game.Scenario.Style: Strikethrough :: StyleFlag
+ Swarm.Game.Scenario.Style: Underline :: StyleFlag
+ Swarm.Game.Scenario.Style: [bg] :: CustomAttr -> Maybe HexColor
+ Swarm.Game.Scenario.Style: [fg] :: CustomAttr -> Maybe HexColor
+ Swarm.Game.Scenario.Style: [name] :: CustomAttr -> String
+ Swarm.Game.Scenario.Style: [style] :: CustomAttr -> Maybe (Set StyleFlag)
+ Swarm.Game.Scenario.Style: data CustomAttr
+ Swarm.Game.Scenario.Style: data StyleFlag
+ Swarm.Game.Scenario.Style: instance Data.Aeson.Types.FromJSON.FromJSON Swarm.Game.Scenario.Style.CustomAttr
+ Swarm.Game.Scenario.Style: instance Data.Aeson.Types.FromJSON.FromJSON Swarm.Game.Scenario.Style.HexColor
+ Swarm.Game.Scenario.Style: instance Data.Aeson.Types.FromJSON.FromJSON Swarm.Game.Scenario.Style.StyleFlag
+ Swarm.Game.Scenario.Style: instance GHC.Classes.Eq Swarm.Game.Scenario.Style.CustomAttr
+ Swarm.Game.Scenario.Style: instance GHC.Classes.Eq Swarm.Game.Scenario.Style.HexColor
+ Swarm.Game.Scenario.Style: instance GHC.Classes.Eq Swarm.Game.Scenario.Style.StyleFlag
+ Swarm.Game.Scenario.Style: instance GHC.Classes.Ord Swarm.Game.Scenario.Style.StyleFlag
+ Swarm.Game.Scenario.Style: instance GHC.Generics.Generic Swarm.Game.Scenario.Style.CustomAttr
+ Swarm.Game.Scenario.Style: instance GHC.Generics.Generic Swarm.Game.Scenario.Style.HexColor
+ Swarm.Game.Scenario.Style: instance GHC.Generics.Generic Swarm.Game.Scenario.Style.StyleFlag
+ Swarm.Game.Scenario.Style: instance GHC.Show.Show Swarm.Game.Scenario.Style.CustomAttr
+ Swarm.Game.Scenario.Style: instance GHC.Show.Show Swarm.Game.Scenario.Style.HexColor
+ Swarm.Game.Scenario.Style: instance GHC.Show.Show Swarm.Game.Scenario.Style.StyleFlag
+ Swarm.Game.Scenario.Style: newtype HexColor
+ Swarm.Game.Scenario.Style: styleFlagJsonOptions :: Options
+ Swarm.Game.Scenario.WorldDescription: WorldDescription :: Maybe (PCell e) -> Bool -> WorldPalette e -> Location -> [[PCell e]] -> PWorldDescription e
+ Swarm.Game.Scenario.WorldDescription: WorldPalette :: KeyMap (PCell e) -> WorldPalette e
+ Swarm.Game.Scenario.WorldDescription: [area] :: PWorldDescription e -> [[PCell e]]
+ Swarm.Game.Scenario.WorldDescription: [defaultTerrain] :: PWorldDescription e -> Maybe (PCell e)
+ Swarm.Game.Scenario.WorldDescription: [offsetOrigin] :: PWorldDescription e -> Bool
+ Swarm.Game.Scenario.WorldDescription: [palette] :: PWorldDescription e -> WorldPalette e
+ Swarm.Game.Scenario.WorldDescription: [ul] :: PWorldDescription e -> Location
+ Swarm.Game.Scenario.WorldDescription: [unPalette] :: WorldPalette e -> KeyMap (PCell e)
+ Swarm.Game.Scenario.WorldDescription: data PWorldDescription e
+ Swarm.Game.Scenario.WorldDescription: instance GHC.Classes.Eq e => GHC.Classes.Eq (Swarm.Game.Scenario.WorldDescription.PWorldDescription e)
+ Swarm.Game.Scenario.WorldDescription: instance GHC.Classes.Eq e => GHC.Classes.Eq (Swarm.Game.Scenario.WorldDescription.WorldPalette e)
+ Swarm.Game.Scenario.WorldDescription: instance GHC.Show.Show e => GHC.Show.Show (Swarm.Game.Scenario.WorldDescription.PWorldDescription e)
+ Swarm.Game.Scenario.WorldDescription: instance GHC.Show.Show e => GHC.Show.Show (Swarm.Game.Scenario.WorldDescription.WorldPalette e)
+ Swarm.Game.Scenario.WorldDescription: instance Swarm.Util.Yaml.FromJSONE (Swarm.Game.Entity.EntityMap, Swarm.Game.Scenario.RobotLookup.RobotMap) (Swarm.Game.Scenario.WorldDescription.WorldPalette Swarm.Game.Entity.Entity)
+ Swarm.Game.Scenario.WorldDescription: instance Swarm.Util.Yaml.FromJSONE (Swarm.Game.Entity.EntityMap, Swarm.Game.Scenario.RobotLookup.RobotMap) Swarm.Game.Scenario.WorldDescription.WorldDescription
+ Swarm.Game.Scenario.WorldDescription: newtype WorldPalette e
+ Swarm.Game.Scenario.WorldDescription: paintMap :: MonadFail m => WorldPalette e -> Text -> m [[PCell e]]
+ Swarm.Game.Scenario.WorldDescription: type WorldDescription = PWorldDescription Entity
+ Swarm.Game.State: ObjectiveCompleted :: Objective -> Announcement
+ Swarm.Game.State: ObjectiveCompletion :: CompletionBuckets -> Set ObjectiveLabel -> ObjectiveCompletion
+ Swarm.Game.State: Ongoing :: WinStatus
+ Swarm.Game.State: Unwinnable :: Bool -> WinStatus
+ Swarm.Game.State: [completedIDs] :: ObjectiveCompletion -> Set ObjectiveLabel
+ Swarm.Game.State: [completionBuckets] :: ObjectiveCompletion -> CompletionBuckets
+ Swarm.Game.State: announcementQueue :: Lens' GameState (Seq Announcement)
+ Swarm.Game.State: data ObjectiveCompletion
+ Swarm.Game.State: data WinStatus
+ Swarm.Game.State: gameAchievements :: Lens' GameState (Map GameplayAchievement Attainment)
+ Swarm.Game.State: instance Data.Aeson.Types.FromJSON.FromJSON Swarm.Game.State.WinStatus
+ Swarm.Game.State: instance Data.Aeson.Types.ToJSON.ToJSON Swarm.Game.State.WinStatus
+ Swarm.Game.State: instance GHC.Generics.Generic Swarm.Game.State.WinStatus
+ Swarm.Game.State: instance GHC.Show.Show Swarm.Game.State.WinStatus
+ Swarm.Game.State: newtype Announcement
+ Swarm.Game.Step: CompletionsWithExceptions :: [Text] -> ObjectiveCompletion -> [Objective] -> CompletionsWithExceptions
+ Swarm.Game.Step: [completionAnnouncementQueue] :: CompletionsWithExceptions -> [Objective]
+ Swarm.Game.Step: [completions] :: CompletionsWithExceptions -> ObjectiveCompletion
+ Swarm.Game.Step: [exceptions] :: CompletionsWithExceptions -> [Text]
+ Swarm.Game.Step: cmdExnWithAchievement :: Const -> [Text] -> GameplayAchievement -> Exn
+ Swarm.Game.Step: data CompletionsWithExceptions
+ Swarm.Game.Step: formatDevices :: Set Entity -> Text
+ Swarm.Game.Step: hypotheticalWinCheck :: (Has (State GameState) sig m, Has (Lift IO) sig m) => EntityMap -> GameState -> WinStatus -> ObjectiveCompletion -> m ()
+ Swarm.Game.Step: updateWorld :: (Has (State GameState) sig m, Has (Throw Exn) sig m) => Const -> WorldUpdate Entity -> m ()
+ Swarm.Game.World: ReplaceEntity :: Location -> e -> Maybe e -> WorldUpdate e
+ Swarm.Game.World: [newEntity] :: WorldUpdate e -> Maybe e
+ Swarm.Game.World: [originalEntity] :: WorldUpdate e -> e
+ Swarm.Game.World: [updatedLoc] :: WorldUpdate e -> Location
+ Swarm.Game.World: data WorldUpdate e
+ Swarm.Game.World: instance Data.Aeson.Types.FromJSON.FromJSON e => Data.Aeson.Types.FromJSON.FromJSON (Swarm.Game.World.WorldUpdate e)
+ Swarm.Game.World: instance Data.Aeson.Types.ToJSON.ToJSON e => Data.Aeson.Types.ToJSON.ToJSON (Swarm.Game.World.WorldUpdate e)
+ Swarm.Game.World: instance GHC.Classes.Eq e => GHC.Classes.Eq (Swarm.Game.World.WorldUpdate e)
+ Swarm.Game.World: instance GHC.Classes.Ord e => GHC.Classes.Ord (Swarm.Game.World.WorldUpdate e)
+ Swarm.Game.World: instance GHC.Generics.Generic (Swarm.Game.World.WorldUpdate e)
+ Swarm.Game.World: instance GHC.Show.Show e => GHC.Show.Show (Swarm.Game.World.WorldUpdate e)
+ Swarm.Language.Capability: CCode :: Capability
+ Swarm.Language.Capability: CEquip :: Capability
+ Swarm.Language.Capability: CMeet :: Capability
+ Swarm.Language.Capability: CUnequip :: Capability
+ Swarm.Language.Elaborate: sForce :: Syntax' Polytype
+ Swarm.Language.LSP: diagnosticSourcePrefix :: Text
+ Swarm.Language.LSP.Hover: explain :: ExplainableType ty => Syntax' ty -> Tree Text
+ Swarm.Language.LSP.Hover: instance Swarm.Language.LSP.Hover.ExplainableType ()
+ Swarm.Language.LSP.Hover: instance Swarm.Language.LSP.Hover.ExplainableType Swarm.Language.Types.Polytype
+ Swarm.Language.LSP.Hover: narrowToPosition :: ExplainableType ty => Syntax' ty -> Int -> Syntax' ty
+ Swarm.Language.LSP.Hover: renderDoc :: Int -> Text -> Text
+ Swarm.Language.LSP.Hover: showHoverInfo :: NormalizedUri -> TextDocumentVersion -> Position -> VirtualFile -> Maybe (Text, Maybe Range)
+ Swarm.Language.LSP.Hover: treeToMarkdown :: Int -> Tree Text -> Text
+ Swarm.Language.LSP.VarUsage: Bind :: BindingType
+ Swarm.Language.LSP.VarUsage: Lambda :: BindingType
+ Swarm.Language.LSP.VarUsage: Let :: BindingType
+ Swarm.Language.LSP.VarUsage: Usage :: Set LocVar -> [VarUsage] -> Usage
+ Swarm.Language.LSP.VarUsage: VarUsage :: LocVar -> BindingType -> VarUsage
+ Swarm.Language.LSP.VarUsage: [problems] :: Usage -> [VarUsage]
+ Swarm.Language.LSP.VarUsage: [usages] :: Usage -> Set LocVar
+ Swarm.Language.LSP.VarUsage: checkOccurrences :: BindingSites -> LocVar -> BindingType -> [Syntax] -> Usage
+ Swarm.Language.LSP.VarUsage: data BindingType
+ Swarm.Language.LSP.VarUsage: data Usage
+ Swarm.Language.LSP.VarUsage: data VarUsage
+ Swarm.Language.LSP.VarUsage: getUsage :: BindingSites -> Syntax -> Usage
+ Swarm.Language.LSP.VarUsage: instance GHC.Base.Monoid Swarm.Language.LSP.VarUsage.Usage
+ Swarm.Language.LSP.VarUsage: instance GHC.Base.Semigroup Swarm.Language.LSP.VarUsage.Usage
+ Swarm.Language.LSP.VarUsage: instance GHC.Classes.Eq Swarm.Language.LSP.VarUsage.BindingType
+ Swarm.Language.LSP.VarUsage: instance GHC.Show.Show Swarm.Language.LSP.VarUsage.BindingType
+ Swarm.Language.LSP.VarUsage: toErrPos :: Text -> VarUsage -> Maybe (Range, Text)
+ Swarm.Language.LSP.VarUsage: type BindingSites = Map Var (NonEmpty SrcLoc)
+ Swarm.Language.Module: Module :: Syntax' s -> Ctx t -> Module s t
+ Swarm.Language.Module: [moduleAST] :: Module s t -> Syntax' s
+ Swarm.Language.Module: [moduleCtx] :: Module s t -> Ctx t
+ Swarm.Language.Module: data Module s t
+ Swarm.Language.Module: instance (Data.Aeson.Types.FromJSON.FromJSON s, Data.Aeson.Types.FromJSON.FromJSON t) => Data.Aeson.Types.FromJSON.FromJSON (Swarm.Language.Module.Module s t)
+ Swarm.Language.Module: instance (Data.Aeson.Types.ToJSON.ToJSON t, Data.Aeson.Types.ToJSON.ToJSON s) => Data.Aeson.Types.ToJSON.ToJSON (Swarm.Language.Module.Module s t)
+ Swarm.Language.Module: instance (Data.Data.Data s, Data.Data.Data t) => Data.Data.Data (Swarm.Language.Module.Module s t)
+ Swarm.Language.Module: instance (GHC.Classes.Eq s, GHC.Classes.Eq t) => GHC.Classes.Eq (Swarm.Language.Module.Module s t)
+ Swarm.Language.Module: instance (GHC.Show.Show s, GHC.Show.Show t) => GHC.Show.Show (Swarm.Language.Module.Module s t)
+ Swarm.Language.Module: instance GHC.Base.Functor (Swarm.Language.Module.Module s)
+ Swarm.Language.Module: instance GHC.Generics.Generic (Swarm.Language.Module.Module s t)
+ Swarm.Language.Module: trivMod :: Syntax' s -> Module s t
+ Swarm.Language.Module: type TModule = Module Polytype Polytype
+ Swarm.Language.Module: type UModule = Module UType UPolytype
+ Swarm.Language.Parse: unTuple :: Syntax' ty -> [Syntax' ty]
+ Swarm.Language.Pretty: instance Swarm.Language.Pretty.PrettyPrec (Swarm.Language.Syntax.Syntax' ty)
+ Swarm.Language.Syntax: CharAt :: Const
+ Swarm.Language.Syntax: DAbsolute :: AbsoluteDir -> Direction
+ Swarm.Language.Syntax: DRelative :: RelativeDir -> Direction
+ Swarm.Language.Syntax: Equip :: Const
+ Swarm.Language.Syntax: Equipped :: Const
+ Swarm.Language.Syntax: Heading :: Const
+ Swarm.Language.Syntax: Isempty :: Const
+ Swarm.Language.Syntax: LV :: SrcLoc -> Var -> LocVar
+ Swarm.Language.Syntax: Meet :: Const
+ Swarm.Language.Syntax: MeetAll :: Const
+ Swarm.Language.Syntax: SrcLoc :: Int -> Int -> SrcLoc
+ Swarm.Language.Syntax: Syntax' :: SrcLoc -> Term' ty -> ty -> Syntax' ty
+ Swarm.Language.Syntax: ToChar :: Const
+ Swarm.Language.Syntax: Unequip :: Const
+ Swarm.Language.Syntax: [_sLoc] :: Syntax' ty -> SrcLoc
+ Swarm.Language.Syntax: [_sTerm] :: Syntax' ty -> Term' ty
+ Swarm.Language.Syntax: [_sType] :: Syntax' ty -> ty
+ Swarm.Language.Syntax: [lvSrcLoc] :: LocVar -> SrcLoc
+ Swarm.Language.Syntax: [lvVar] :: LocVar -> Var
+ Swarm.Language.Syntax: data AbsoluteDir
+ Swarm.Language.Syntax: data LocVar
+ Swarm.Language.Syntax: data RelativeDir
+ Swarm.Language.Syntax: data SrcLoc
+ Swarm.Language.Syntax: data Syntax' ty
+ Swarm.Language.Syntax: data Term' ty
+ Swarm.Language.Syntax: erase :: Term' ty -> Term
+ Swarm.Language.Syntax: eraseS :: Syntax' ty -> Term
+ Swarm.Language.Syntax: freeVarsS :: forall ty. Traversal' (Syntax' ty) (Syntax' ty)
+ Swarm.Language.Syntax: freeVarsT :: forall ty. Traversal' (Syntax' ty) (Term' ty)
+ Swarm.Language.Syntax: freeVarsV :: Traversal' (Syntax' ty) Var
+ Swarm.Language.Syntax: instance Data.Aeson.Types.FromJSON.FromJSON Swarm.Language.Syntax.AbsoluteDir
+ Swarm.Language.Syntax: instance Data.Aeson.Types.FromJSON.FromJSON Swarm.Language.Syntax.LocVar
+ Swarm.Language.Syntax: instance Data.Aeson.Types.FromJSON.FromJSON Swarm.Language.Syntax.RelativeDir
+ Swarm.Language.Syntax: instance Data.Aeson.Types.FromJSON.FromJSON Swarm.Language.Syntax.SrcLoc
+ Swarm.Language.Syntax: instance Data.Aeson.Types.FromJSON.FromJSON ty => Data.Aeson.Types.FromJSON.FromJSON (Swarm.Language.Syntax.Syntax' ty)
+ Swarm.Language.Syntax: instance Data.Aeson.Types.FromJSON.FromJSON ty => Data.Aeson.Types.FromJSON.FromJSON (Swarm.Language.Syntax.Term' ty)
+ Swarm.Language.Syntax: instance Data.Aeson.Types.FromJSON.FromJSONKey Swarm.Language.Syntax.AbsoluteDir
+ Swarm.Language.Syntax: instance Data.Aeson.Types.ToJSON.ToJSON Swarm.Language.Syntax.AbsoluteDir
+ Swarm.Language.Syntax: instance Data.Aeson.Types.ToJSON.ToJSON Swarm.Language.Syntax.LocVar
+ Swarm.Language.Syntax: instance Data.Aeson.Types.ToJSON.ToJSON Swarm.Language.Syntax.RelativeDir
+ Swarm.Language.Syntax: instance Data.Aeson.Types.ToJSON.ToJSON Swarm.Language.Syntax.SrcLoc
+ Swarm.Language.Syntax: instance Data.Aeson.Types.ToJSON.ToJSON ty => Data.Aeson.Types.ToJSON.ToJSON (Swarm.Language.Syntax.Syntax' ty)
+ Swarm.Language.Syntax: instance Data.Aeson.Types.ToJSON.ToJSON ty => Data.Aeson.Types.ToJSON.ToJSON (Swarm.Language.Syntax.Term' ty)
+ Swarm.Language.Syntax: instance Data.Aeson.Types.ToJSON.ToJSONKey Swarm.Language.Syntax.AbsoluteDir
+ Swarm.Language.Syntax: instance Data.Data.Data Swarm.Language.Syntax.AbsoluteDir
+ Swarm.Language.Syntax: instance Data.Data.Data Swarm.Language.Syntax.LocVar
+ Swarm.Language.Syntax: instance Data.Data.Data Swarm.Language.Syntax.RelativeDir
+ Swarm.Language.Syntax: instance Data.Data.Data Swarm.Language.Syntax.SrcLoc
+ Swarm.Language.Syntax: instance Data.Data.Data ty => Control.Lens.Plated.Plated (Swarm.Language.Syntax.Syntax' ty)
+ Swarm.Language.Syntax: instance Data.Data.Data ty => Control.Lens.Plated.Plated (Swarm.Language.Syntax.Term' ty)
+ Swarm.Language.Syntax: instance Data.Data.Data ty => Data.Data.Data (Swarm.Language.Syntax.Syntax' ty)
+ Swarm.Language.Syntax: instance Data.Data.Data ty => Data.Data.Data (Swarm.Language.Syntax.Term' ty)
+ Swarm.Language.Syntax: instance Data.Foldable.Foldable Swarm.Language.Syntax.Syntax'
+ Swarm.Language.Syntax: instance Data.Foldable.Foldable Swarm.Language.Syntax.Term'
+ Swarm.Language.Syntax: instance Data.Hashable.Class.Hashable Swarm.Language.Syntax.AbsoluteDir
+ Swarm.Language.Syntax: instance Data.Hashable.Class.Hashable Swarm.Language.Syntax.RelativeDir
+ Swarm.Language.Syntax: instance Data.Traversable.Traversable Swarm.Language.Syntax.Syntax'
+ Swarm.Language.Syntax: instance Data.Traversable.Traversable Swarm.Language.Syntax.Term'
+ Swarm.Language.Syntax: instance GHC.Base.Functor Swarm.Language.Syntax.Syntax'
+ Swarm.Language.Syntax: instance GHC.Base.Functor Swarm.Language.Syntax.Term'
+ Swarm.Language.Syntax: instance GHC.Base.Monoid Swarm.Language.Syntax.SrcLoc
+ Swarm.Language.Syntax: instance GHC.Base.Semigroup Swarm.Language.Syntax.SrcLoc
+ Swarm.Language.Syntax: instance GHC.Classes.Eq Swarm.Language.Syntax.AbsoluteDir
+ Swarm.Language.Syntax: instance GHC.Classes.Eq Swarm.Language.Syntax.LocVar
+ Swarm.Language.Syntax: instance GHC.Classes.Eq Swarm.Language.Syntax.RelativeDir
+ Swarm.Language.Syntax: instance GHC.Classes.Eq Swarm.Language.Syntax.SrcLoc
+ Swarm.Language.Syntax: instance GHC.Classes.Eq ty => GHC.Classes.Eq (Swarm.Language.Syntax.Syntax' ty)
+ Swarm.Language.Syntax: instance GHC.Classes.Eq ty => GHC.Classes.Eq (Swarm.Language.Syntax.Term' ty)
+ Swarm.Language.Syntax: instance GHC.Classes.Ord Swarm.Language.Syntax.AbsoluteDir
+ Swarm.Language.Syntax: instance GHC.Classes.Ord Swarm.Language.Syntax.LocVar
+ Swarm.Language.Syntax: instance GHC.Classes.Ord Swarm.Language.Syntax.RelativeDir
+ Swarm.Language.Syntax: instance GHC.Classes.Ord Swarm.Language.Syntax.SrcLoc
+ Swarm.Language.Syntax: instance GHC.Enum.Bounded Swarm.Language.Syntax.AbsoluteDir
+ Swarm.Language.Syntax: instance GHC.Enum.Bounded Swarm.Language.Syntax.RelativeDir
+ Swarm.Language.Syntax: instance GHC.Enum.Enum Swarm.Language.Syntax.AbsoluteDir
+ Swarm.Language.Syntax: instance GHC.Enum.Enum Swarm.Language.Syntax.RelativeDir
+ Swarm.Language.Syntax: instance GHC.Generics.Generic (Swarm.Language.Syntax.Syntax' ty)
+ Swarm.Language.Syntax: instance GHC.Generics.Generic (Swarm.Language.Syntax.Term' ty)
+ Swarm.Language.Syntax: instance GHC.Generics.Generic Swarm.Language.Syntax.AbsoluteDir
+ Swarm.Language.Syntax: instance GHC.Generics.Generic Swarm.Language.Syntax.LocVar
+ Swarm.Language.Syntax: instance GHC.Generics.Generic Swarm.Language.Syntax.RelativeDir
+ Swarm.Language.Syntax: instance GHC.Generics.Generic Swarm.Language.Syntax.SrcLoc
+ Swarm.Language.Syntax: instance GHC.Read.Read Swarm.Language.Syntax.AbsoluteDir
+ Swarm.Language.Syntax: instance GHC.Read.Read Swarm.Language.Syntax.RelativeDir
+ Swarm.Language.Syntax: instance GHC.Show.Show Swarm.Language.Syntax.AbsoluteDir
+ Swarm.Language.Syntax: instance GHC.Show.Show Swarm.Language.Syntax.LocVar
+ Swarm.Language.Syntax: instance GHC.Show.Show Swarm.Language.Syntax.RelativeDir
+ Swarm.Language.Syntax: instance GHC.Show.Show Swarm.Language.Syntax.SrcLoc
+ Swarm.Language.Syntax: instance GHC.Show.Show ty => GHC.Show.Show (Swarm.Language.Syntax.Syntax' ty)
+ Swarm.Language.Syntax: instance GHC.Show.Show ty => GHC.Show.Show (Swarm.Language.Syntax.Term' ty)
+ Swarm.Language.Syntax: locVarToSyntax' :: LocVar -> ty -> Syntax' ty
+ Swarm.Language.Syntax: mapFreeS :: Var -> (Syntax' ty -> Syntax' ty) -> Syntax' ty -> Syntax' ty
+ Swarm.Language.Syntax: pattern Syntax :: SrcLoc -> Term -> Syntax
+ Swarm.Language.Syntax: sLoc :: forall ty_a1DXw. Lens' (Syntax' ty_a1DXw) SrcLoc
+ Swarm.Language.Syntax: sTerm :: forall ty_a1DXw. Lens' (Syntax' ty_a1DXw) (Term' ty_a1DXw)
+ Swarm.Language.Syntax: sType :: forall ty_a1DXw. Lens' (Syntax' ty_a1DXw) ty_a1DXw
+ Swarm.Language.Syntax: type Syntax = Syntax' ()
+ Swarm.Language.Syntax: type Term = Term' ()
+ Swarm.Language.Syntax: unfoldApps :: Syntax' ty -> NonEmpty (Syntax' ty)
+ Swarm.Language.Typecheck: getTypeErrSrcLoc :: TypeErr -> Maybe SrcLoc
+ Swarm.Language.Typecheck: instance (Swarm.Language.Typecheck.HasBindings u, Data.Data.Data u) => Swarm.Language.Typecheck.HasBindings (Swarm.Language.Syntax.Syntax' u)
+ Swarm.Language.Typecheck: instance (Swarm.Language.Typecheck.HasBindings u, Data.Data.Data u) => Swarm.Language.Typecheck.HasBindings (Swarm.Language.Syntax.Term' u)
+ Swarm.Language.Typecheck: instance Swarm.Language.Typecheck.HasBindings Swarm.Language.Module.UModule
+ Swarm.Language.Types: BActor :: BaseTy
+ Swarm.Language.Types: pattern TyActor :: Type
+ Swarm.Language.Types: pattern UTyActor :: UType
+ Swarm.Language.Value: [VBind] :: Maybe Var -> Term -> Term -> Env -> Value
+ Swarm.Language.Value: [VBool] :: Bool -> Value
+ Swarm.Language.Value: [VCApp] :: Const -> [Value] -> Value
+ Swarm.Language.Value: [VClo] :: Var -> Term -> Env -> Value
+ Swarm.Language.Value: [VDef] :: Bool -> Var -> Term -> Env -> Value
+ Swarm.Language.Value: [VDelay] :: Term -> Env -> Value
+ Swarm.Language.Value: [VDir] :: Direction -> Value
+ Swarm.Language.Value: [VInj] :: Bool -> Value -> Value
+ Swarm.Language.Value: [VInt] :: Integer -> Value
+ Swarm.Language.Value: [VPair] :: Value -> Value -> Value
+ Swarm.Language.Value: [VRef] :: Int -> Value
+ Swarm.Language.Value: [VResult] :: Value -> Env -> Value
+ Swarm.Language.Value: [VRobot] :: Int -> Value
+ Swarm.Language.Value: [VText] :: Text -> Value
+ Swarm.Language.Value: [VUnit] :: Value
+ Swarm.Language.Value: data Value
+ Swarm.Language.Value: instance Data.Aeson.Types.FromJSON.FromJSON Swarm.Language.Value.Value
+ Swarm.Language.Value: instance Data.Aeson.Types.ToJSON.ToJSON Swarm.Language.Value.Value
+ Swarm.Language.Value: instance GHC.Classes.Eq Swarm.Language.Value.Value
+ Swarm.Language.Value: instance GHC.Generics.Generic Swarm.Language.Value.Value
+ Swarm.Language.Value: instance GHC.Show.Show Swarm.Language.Value.Value
+ Swarm.Language.Value: prettyValue :: Value -> Text
+ Swarm.Language.Value: stripVResult :: Value -> Value
+ Swarm.Language.Value: type Env = Ctx Value
+ Swarm.Language.Value: valueToTerm :: Value -> Term
+ Swarm.TUI.Attr: lightCyanAttr :: AttrName
+ Swarm.TUI.Attr: magentaAttr :: AttrName
+ Swarm.TUI.Controller.Util: isRunningModal :: ModalType -> Bool
+ Swarm.TUI.Controller.Util: openModal :: ModalType -> EventM Name AppState ()
+ Swarm.TUI.Controller.Util: pattern CharKey :: Char -> BrickEvent n e
+ Swarm.TUI.Controller.Util: pattern ControlChar :: Char -> BrickEvent n e
+ Swarm.TUI.Controller.Util: pattern EscapeKey :: BrickEvent n e
+ Swarm.TUI.Controller.Util: pattern FKey :: Int -> BrickEvent n e
+ Swarm.TUI.Controller.Util: pattern Key :: Key -> BrickEvent n e
+ Swarm.TUI.Controller.Util: pattern MetaChar :: Char -> BrickEvent n e
+ Swarm.TUI.Controller.Util: pattern ShiftKey :: Key -> BrickEvent n e
+ Swarm.TUI.Controller.Util: setFocus :: FocusablePanel -> EventM Name AppState ()
+ Swarm.TUI.Model: AchievementList :: Name
+ Swarm.TUI.Model: Achievements :: MainMenuEntry
+ Swarm.TUI.Model: AchievementsMenu :: List Name CategorizedAchievement -> Menu
+ Swarm.TUI.Model: AppState :: GameState -> UIState -> RuntimeState -> AppState
+ Swarm.TUI.Model: Button :: Button -> Name
+ Swarm.TUI.Model: Cancel :: ButtonAction
+ Swarm.TUI.Model: ColorMode16 :: ColorMode
+ Swarm.TUI.Model: ColorMode240 :: !Word8 -> ColorMode
+ Swarm.TUI.Model: ColorMode8 :: ColorMode
+ Swarm.TUI.Model: EquippedEntry :: Entity -> InventoryListEntry
+ Swarm.TUI.Model: FocusablePanel :: FocusablePanel -> Name
+ Swarm.TUI.Model: FullColor :: ColorMode
+ Swarm.TUI.Model: GoalWidgets :: GoalWidget -> Name
+ Swarm.TUI.Model: KeepPlaying :: ButtonAction
+ Swarm.TUI.Model: LoseModal :: ModalType
+ Swarm.TUI.Model: Next :: ScenarioInfoPair -> ButtonAction
+ Swarm.TUI.Model: NoColor :: ColorMode
+ Swarm.TUI.Model: Piloting :: ReplControlMode
+ Swarm.TUI.Model: QuitAction :: ButtonAction
+ Swarm.TUI.Model: StartOver :: Seed -> ScenarioInfoPair -> ButtonAction
+ Swarm.TUI.Model: Typing :: ReplControlMode
+ Swarm.TUI.Model: [colorMode] :: AppOpts -> Maybe ColorMode
+ Swarm.TUI.Model: _EquippedEntry :: Prism' InventoryListEntry Entity
+ Swarm.TUI.Model: data Button
+ Swarm.TUI.Model: data ButtonAction
+ Swarm.TUI.Model: data ColorMode
+ Swarm.TUI.Model: data FocusablePanel
+ Swarm.TUI.Model: data ReplControlMode
+ Swarm.TUI.Model: replControlMode :: Lens' REPLState ReplControlMode
+ Swarm.TUI.Model.Achievement.Attainment: Attainment :: CategorizedAchievement -> Maybe FilePath -> ZonedTime -> Attainment
+ Swarm.TUI.Model.Achievement.Attainment: [_achievement] :: Attainment -> CategorizedAchievement
+ Swarm.TUI.Model.Achievement.Attainment: [_maybeScenarioPath] :: Attainment -> Maybe FilePath
+ Swarm.TUI.Model.Achievement.Attainment: [_obtainedAt] :: Attainment -> ZonedTime
+ Swarm.TUI.Model.Achievement.Attainment: achievement :: Lens' Attainment CategorizedAchievement
+ Swarm.TUI.Model.Achievement.Attainment: achievementJsonOptions :: Options
+ Swarm.TUI.Model.Achievement.Attainment: data Attainment
+ Swarm.TUI.Model.Achievement.Attainment: instance Data.Aeson.Types.FromJSON.FromJSON Swarm.TUI.Model.Achievement.Attainment.Attainment
+ Swarm.TUI.Model.Achievement.Attainment: instance Data.Aeson.Types.ToJSON.ToJSON Swarm.TUI.Model.Achievement.Attainment.Attainment
+ Swarm.TUI.Model.Achievement.Attainment: instance GHC.Base.Semigroup Swarm.TUI.Model.Achievement.Attainment.Attainment
+ Swarm.TUI.Model.Achievement.Attainment: instance GHC.Classes.Eq Swarm.TUI.Model.Achievement.Attainment.Attainment
+ Swarm.TUI.Model.Achievement.Attainment: instance GHC.Classes.Ord Swarm.TUI.Model.Achievement.Attainment.Attainment
+ Swarm.TUI.Model.Achievement.Attainment: instance GHC.Generics.Generic Swarm.TUI.Model.Achievement.Attainment.Attainment
+ Swarm.TUI.Model.Achievement.Attainment: maybeScenarioPath :: Lens' Attainment (Maybe FilePath)
+ Swarm.TUI.Model.Achievement.Attainment: obtainedAt :: Lens' Attainment ZonedTime
+ Swarm.TUI.Model.Achievement.Definitions: AchievementInfo :: Text -> Maybe FlavorText -> Text -> ExpectedEffort -> Bool -> AchievementInfo
+ Swarm.TUI.Model.Achievement.Definitions: AttemptSelfDestructBase :: GameplayAchievement
+ Swarm.TUI.Model.Achievement.Definitions: CompletedAllTutorials :: GlobalAchievement
+ Swarm.TUI.Model.Achievement.Definitions: CompletedSingleTutorial :: GlobalAchievement
+ Swarm.TUI.Model.Achievement.Definitions: CraftedBitcoin :: GameplayAchievement
+ Swarm.TUI.Model.Achievement.Definitions: DestroyedBase :: GameplayAchievement
+ Swarm.TUI.Model.Achievement.Definitions: Easy :: ExpectedEffort
+ Swarm.TUI.Model.Achievement.Definitions: FTQuotation :: Quotation -> FlavorText
+ Swarm.TUI.Model.Achievement.Definitions: Freeform :: Text -> FlavorText
+ Swarm.TUI.Model.Achievement.Definitions: GameplayAchievement :: GameplayAchievement -> CategorizedAchievement
+ Swarm.TUI.Model.Achievement.Definitions: GlobalAchievement :: GlobalAchievement -> CategorizedAchievement
+ Swarm.TUI.Model.Achievement.Definitions: Gruelling :: ExpectedEffort
+ Swarm.TUI.Model.Achievement.Definitions: LookedAtAboutScreen :: GlobalAchievement
+ Swarm.TUI.Model.Achievement.Definitions: LoseScenario :: GameplayAchievement
+ Swarm.TUI.Model.Achievement.Definitions: Moderate :: ExpectedEffort
+ Swarm.TUI.Model.Achievement.Definitions: Quotation :: Text -> Text -> Quotation
+ Swarm.TUI.Model.Achievement.Definitions: RobotIntoWater :: GameplayAchievement
+ Swarm.TUI.Model.Achievement.Definitions: Trivial :: ExpectedEffort
+ Swarm.TUI.Model.Achievement.Definitions: [attainmentProcess] :: AchievementInfo -> Text
+ Swarm.TUI.Model.Achievement.Definitions: [attribution] :: Quotation -> Text
+ Swarm.TUI.Model.Achievement.Definitions: [content] :: Quotation -> Text
+ Swarm.TUI.Model.Achievement.Definitions: [effort] :: AchievementInfo -> ExpectedEffort
+ Swarm.TUI.Model.Achievement.Definitions: [humorousElaboration] :: AchievementInfo -> Maybe FlavorText
+ Swarm.TUI.Model.Achievement.Definitions: [isObfuscated] :: AchievementInfo -> Bool
+ Swarm.TUI.Model.Achievement.Definitions: [title] :: AchievementInfo -> Text
+ Swarm.TUI.Model.Achievement.Definitions: categorizedAchievementJsonOptions :: Options
+ Swarm.TUI.Model.Achievement.Definitions: data AchievementInfo
+ Swarm.TUI.Model.Achievement.Definitions: data CategorizedAchievement
+ Swarm.TUI.Model.Achievement.Definitions: data ExpectedEffort
+ Swarm.TUI.Model.Achievement.Definitions: data FlavorText
+ Swarm.TUI.Model.Achievement.Definitions: data GameplayAchievement
+ Swarm.TUI.Model.Achievement.Definitions: data GlobalAchievement
+ Swarm.TUI.Model.Achievement.Definitions: data Quotation
+ Swarm.TUI.Model.Achievement.Definitions: instance Data.Aeson.Types.FromJSON.FromJSON Swarm.TUI.Model.Achievement.Definitions.AchievementInfo
+ Swarm.TUI.Model.Achievement.Definitions: instance Data.Aeson.Types.FromJSON.FromJSON Swarm.TUI.Model.Achievement.Definitions.CategorizedAchievement
+ Swarm.TUI.Model.Achievement.Definitions: instance Data.Aeson.Types.FromJSON.FromJSON Swarm.TUI.Model.Achievement.Definitions.ExpectedEffort
+ Swarm.TUI.Model.Achievement.Definitions: instance Data.Aeson.Types.FromJSON.FromJSON Swarm.TUI.Model.Achievement.Definitions.FlavorText
+ Swarm.TUI.Model.Achievement.Definitions: instance Data.Aeson.Types.FromJSON.FromJSON Swarm.TUI.Model.Achievement.Definitions.GameplayAchievement
+ Swarm.TUI.Model.Achievement.Definitions: instance Data.Aeson.Types.FromJSON.FromJSON Swarm.TUI.Model.Achievement.Definitions.GlobalAchievement
+ Swarm.TUI.Model.Achievement.Definitions: instance Data.Aeson.Types.FromJSON.FromJSON Swarm.TUI.Model.Achievement.Definitions.Quotation
+ Swarm.TUI.Model.Achievement.Definitions: instance Data.Aeson.Types.ToJSON.ToJSON Swarm.TUI.Model.Achievement.Definitions.AchievementInfo
+ Swarm.TUI.Model.Achievement.Definitions: instance Data.Aeson.Types.ToJSON.ToJSON Swarm.TUI.Model.Achievement.Definitions.CategorizedAchievement
+ Swarm.TUI.Model.Achievement.Definitions: instance Data.Aeson.Types.ToJSON.ToJSON Swarm.TUI.Model.Achievement.Definitions.ExpectedEffort
+ Swarm.TUI.Model.Achievement.Definitions: instance Data.Aeson.Types.ToJSON.ToJSON Swarm.TUI.Model.Achievement.Definitions.FlavorText
+ Swarm.TUI.Model.Achievement.Definitions: instance Data.Aeson.Types.ToJSON.ToJSON Swarm.TUI.Model.Achievement.Definitions.GameplayAchievement
+ Swarm.TUI.Model.Achievement.Definitions: instance Data.Aeson.Types.ToJSON.ToJSON Swarm.TUI.Model.Achievement.Definitions.GlobalAchievement
+ Swarm.TUI.Model.Achievement.Definitions: instance Data.Aeson.Types.ToJSON.ToJSON Swarm.TUI.Model.Achievement.Definitions.Quotation
+ Swarm.TUI.Model.Achievement.Definitions: instance GHC.Classes.Eq Swarm.TUI.Model.Achievement.Definitions.AchievementInfo
+ Swarm.TUI.Model.Achievement.Definitions: instance GHC.Classes.Eq Swarm.TUI.Model.Achievement.Definitions.CategorizedAchievement
+ Swarm.TUI.Model.Achievement.Definitions: instance GHC.Classes.Eq Swarm.TUI.Model.Achievement.Definitions.ExpectedEffort
+ Swarm.TUI.Model.Achievement.Definitions: instance GHC.Classes.Eq Swarm.TUI.Model.Achievement.Definitions.FlavorText
+ Swarm.TUI.Model.Achievement.Definitions: instance GHC.Classes.Eq Swarm.TUI.Model.Achievement.Definitions.GameplayAchievement
+ Swarm.TUI.Model.Achievement.Definitions: instance GHC.Classes.Eq Swarm.TUI.Model.Achievement.Definitions.GlobalAchievement
+ Swarm.TUI.Model.Achievement.Definitions: instance GHC.Classes.Eq Swarm.TUI.Model.Achievement.Definitions.Quotation
+ Swarm.TUI.Model.Achievement.Definitions: instance GHC.Classes.Ord Swarm.TUI.Model.Achievement.Definitions.CategorizedAchievement
+ Swarm.TUI.Model.Achievement.Definitions: instance GHC.Classes.Ord Swarm.TUI.Model.Achievement.Definitions.ExpectedEffort
+ Swarm.TUI.Model.Achievement.Definitions: instance GHC.Classes.Ord Swarm.TUI.Model.Achievement.Definitions.GameplayAchievement
+ Swarm.TUI.Model.Achievement.Definitions: instance GHC.Classes.Ord Swarm.TUI.Model.Achievement.Definitions.GlobalAchievement
+ Swarm.TUI.Model.Achievement.Definitions: instance GHC.Enum.Bounded Swarm.TUI.Model.Achievement.Definitions.ExpectedEffort
+ Swarm.TUI.Model.Achievement.Definitions: instance GHC.Enum.Bounded Swarm.TUI.Model.Achievement.Definitions.GameplayAchievement
+ Swarm.TUI.Model.Achievement.Definitions: instance GHC.Enum.Bounded Swarm.TUI.Model.Achievement.Definitions.GlobalAchievement
+ Swarm.TUI.Model.Achievement.Definitions: instance GHC.Enum.Enum Swarm.TUI.Model.Achievement.Definitions.ExpectedEffort
+ Swarm.TUI.Model.Achievement.Definitions: instance GHC.Enum.Enum Swarm.TUI.Model.Achievement.Definitions.GameplayAchievement
+ Swarm.TUI.Model.Achievement.Definitions: instance GHC.Enum.Enum Swarm.TUI.Model.Achievement.Definitions.GlobalAchievement
+ Swarm.TUI.Model.Achievement.Definitions: instance GHC.Generics.Generic Swarm.TUI.Model.Achievement.Definitions.AchievementInfo
+ Swarm.TUI.Model.Achievement.Definitions: instance GHC.Generics.Generic Swarm.TUI.Model.Achievement.Definitions.CategorizedAchievement
+ Swarm.TUI.Model.Achievement.Definitions: instance GHC.Generics.Generic Swarm.TUI.Model.Achievement.Definitions.ExpectedEffort
+ Swarm.TUI.Model.Achievement.Definitions: instance GHC.Generics.Generic Swarm.TUI.Model.Achievement.Definitions.FlavorText
+ Swarm.TUI.Model.Achievement.Definitions: instance GHC.Generics.Generic Swarm.TUI.Model.Achievement.Definitions.GameplayAchievement
+ Swarm.TUI.Model.Achievement.Definitions: instance GHC.Generics.Generic Swarm.TUI.Model.Achievement.Definitions.GlobalAchievement
+ Swarm.TUI.Model.Achievement.Definitions: instance GHC.Generics.Generic Swarm.TUI.Model.Achievement.Definitions.Quotation
+ Swarm.TUI.Model.Achievement.Definitions: instance GHC.Show.Show Swarm.TUI.Model.Achievement.Definitions.AchievementInfo
+ Swarm.TUI.Model.Achievement.Definitions: instance GHC.Show.Show Swarm.TUI.Model.Achievement.Definitions.CategorizedAchievement
+ Swarm.TUI.Model.Achievement.Definitions: instance GHC.Show.Show Swarm.TUI.Model.Achievement.Definitions.ExpectedEffort
+ Swarm.TUI.Model.Achievement.Definitions: instance GHC.Show.Show Swarm.TUI.Model.Achievement.Definitions.FlavorText
+ Swarm.TUI.Model.Achievement.Definitions: instance GHC.Show.Show Swarm.TUI.Model.Achievement.Definitions.GameplayAchievement
+ Swarm.TUI.Model.Achievement.Definitions: instance GHC.Show.Show Swarm.TUI.Model.Achievement.Definitions.GlobalAchievement
+ Swarm.TUI.Model.Achievement.Definitions: instance GHC.Show.Show Swarm.TUI.Model.Achievement.Definitions.Quotation
+ Swarm.TUI.Model.Achievement.Definitions: listAchievements :: [CategorizedAchievement]
+ Swarm.TUI.Model.Achievement.Description: describe :: CategorizedAchievement -> AchievementInfo
+ Swarm.TUI.Model.Achievement.Persistence: getSwarmAchievementsPath :: Bool -> IO FilePath
+ Swarm.TUI.Model.Achievement.Persistence: loadAchievementsInfo :: IO ([SystemFailure], [Attainment])
+ Swarm.TUI.Model.Achievement.Persistence: saveAchievementsInfo :: [Attainment] -> IO ()
+ Swarm.TUI.Model.Failure: Achievement :: Asset
+ Swarm.TUI.Model.Failure: AssetNotLoaded :: Asset -> FilePath -> LoadingFailure -> SystemFailure
+ Swarm.TUI.Model.Failure: CanNotParse :: ParseException -> LoadingFailure
+ Swarm.TUI.Model.Failure: Data :: Asset
+ Swarm.TUI.Model.Failure: Directory :: Entry
+ Swarm.TUI.Model.Failure: DoesNotExist :: Entry -> LoadingFailure
+ Swarm.TUI.Model.Failure: EntryNot :: Entry -> LoadingFailure
+ Swarm.TUI.Model.Failure: File :: Entry
+ Swarm.TUI.Model.Failure: History :: Asset
+ Swarm.TUI.Model.Failure: Save :: Asset
+ Swarm.TUI.Model.Failure: data Asset
+ Swarm.TUI.Model.Failure: data Entry
+ Swarm.TUI.Model.Failure: data LoadingFailure
+ Swarm.TUI.Model.Failure: data SystemFailure
+ Swarm.TUI.Model.Failure: instance GHC.Classes.Eq Swarm.TUI.Model.Failure.Asset
+ Swarm.TUI.Model.Failure: instance GHC.Classes.Eq Swarm.TUI.Model.Failure.Entry
+ Swarm.TUI.Model.Failure: instance GHC.Show.Show Swarm.TUI.Model.Failure.Asset
+ Swarm.TUI.Model.Failure: instance GHC.Show.Show Swarm.TUI.Model.Failure.Entry
+ Swarm.TUI.Model.Failure: prettyFailure :: SystemFailure -> Text
+ Swarm.TUI.Model.Failure: prettyLoadingFailure :: LoadingFailure -> Text
+ Swarm.TUI.Model.Failure: tShow :: Show a => a -> Text
+ Swarm.TUI.Model.Failure: tShowLow :: Show a => a -> Text
+ Swarm.TUI.Model.Menu: About :: MainMenuEntry
+ Swarm.TUI.Model.Menu: AboutMenu :: Menu
+ Swarm.TUI.Model.Menu: Achievements :: MainMenuEntry
+ Swarm.TUI.Model.Menu: AchievementsMenu :: List Name CategorizedAchievement -> Menu
+ Swarm.TUI.Model.Menu: Cancel :: ButtonAction
+ Swarm.TUI.Model.Menu: CommandsModal :: ModalType
+ Swarm.TUI.Model.Menu: DescriptionModal :: Entity -> ModalType
+ Swarm.TUI.Model.Menu: EquippedEntry :: Entity -> InventoryListEntry
+ Swarm.TUI.Model.Menu: GoalModal :: ModalType
+ Swarm.TUI.Model.Menu: HelpModal :: ModalType
+ Swarm.TUI.Model.Menu: InventoryEntry :: Count -> Entity -> InventoryListEntry
+ Swarm.TUI.Model.Menu: KeepPlaying :: ButtonAction
+ Swarm.TUI.Model.Menu: KeepPlayingModal :: ModalType
+ Swarm.TUI.Model.Menu: LoseModal :: ModalType
+ Swarm.TUI.Model.Menu: MainMenu :: List Name MainMenuEntry -> Menu
+ Swarm.TUI.Model.Menu: Messages :: MainMenuEntry
+ Swarm.TUI.Model.Menu: MessagesMenu :: Menu
+ Swarm.TUI.Model.Menu: MessagesModal :: ModalType
+ Swarm.TUI.Model.Menu: Modal :: ModalType -> Dialog ButtonAction Name -> Modal
+ Swarm.TUI.Model.Menu: NewGame :: MainMenuEntry
+ Swarm.TUI.Model.Menu: NewGameMenu :: NonEmpty (List Name ScenarioItem) -> Menu
+ Swarm.TUI.Model.Menu: Next :: ScenarioInfoPair -> ButtonAction
+ Swarm.TUI.Model.Menu: NoMenu :: Menu
+ Swarm.TUI.Model.Menu: Quit :: MainMenuEntry
+ Swarm.TUI.Model.Menu: QuitAction :: ButtonAction
+ Swarm.TUI.Model.Menu: QuitModal :: ModalType
+ Swarm.TUI.Model.Menu: RecipesModal :: ModalType
+ Swarm.TUI.Model.Menu: RobotsModal :: ModalType
+ Swarm.TUI.Model.Menu: Separator :: Text -> InventoryListEntry
+ Swarm.TUI.Model.Menu: StartOver :: Seed -> ScenarioInfoPair -> ButtonAction
+ Swarm.TUI.Model.Menu: Tutorial :: MainMenuEntry
+ Swarm.TUI.Model.Menu: WinModal :: ModalType
+ Swarm.TUI.Model.Menu: [_modalDialog] :: Modal -> Dialog ButtonAction Name
+ Swarm.TUI.Model.Menu: [_modalType] :: Modal -> ModalType
+ Swarm.TUI.Model.Menu: _AboutMenu :: Prism' Menu ()
+ Swarm.TUI.Model.Menu: _AchievementsMenu :: Prism' Menu (List Name CategorizedAchievement)
+ Swarm.TUI.Model.Menu: _EquippedEntry :: Prism' InventoryListEntry Entity
+ Swarm.TUI.Model.Menu: _InventoryEntry :: Prism' InventoryListEntry (Count, Entity)
+ Swarm.TUI.Model.Menu: _MainMenu :: Prism' Menu (List Name MainMenuEntry)
+ Swarm.TUI.Model.Menu: _MessagesMenu :: Prism' Menu ()
+ Swarm.TUI.Model.Menu: _NewGameMenu :: Prism' Menu (NonEmpty (List Name ScenarioItem))
+ Swarm.TUI.Model.Menu: _NoMenu :: Prism' Menu ()
+ Swarm.TUI.Model.Menu: _Separator :: Prism' InventoryListEntry Text
+ Swarm.TUI.Model.Menu: data ButtonAction
+ Swarm.TUI.Model.Menu: data InventoryListEntry
+ Swarm.TUI.Model.Menu: data MainMenuEntry
+ Swarm.TUI.Model.Menu: data Menu
+ Swarm.TUI.Model.Menu: data Modal
+ Swarm.TUI.Model.Menu: data ModalType
+ Swarm.TUI.Model.Menu: instance GHC.Classes.Eq Swarm.TUI.Model.Menu.InventoryListEntry
+ Swarm.TUI.Model.Menu: instance GHC.Classes.Eq Swarm.TUI.Model.Menu.MainMenuEntry
+ Swarm.TUI.Model.Menu: instance GHC.Classes.Ord Swarm.TUI.Model.Menu.MainMenuEntry
+ Swarm.TUI.Model.Menu: instance GHC.Enum.Bounded Swarm.TUI.Model.Menu.MainMenuEntry
+ Swarm.TUI.Model.Menu: instance GHC.Enum.Enum Swarm.TUI.Model.Menu.MainMenuEntry
+ Swarm.TUI.Model.Menu: instance GHC.Read.Read Swarm.TUI.Model.Menu.MainMenuEntry
+ Swarm.TUI.Model.Menu: instance GHC.Show.Show Swarm.TUI.Model.Menu.MainMenuEntry
+ Swarm.TUI.Model.Menu: instance GHC.Show.Show Swarm.TUI.Model.Menu.ModalType
+ Swarm.TUI.Model.Menu: mainMenu :: MainMenuEntry -> List Name MainMenuEntry
+ Swarm.TUI.Model.Menu: mkNewGameMenu :: Bool -> ScenarioCollection -> FilePath -> Maybe Menu
+ Swarm.TUI.Model.Menu: mkScenarioList :: Bool -> ScenarioCollection -> List Name ScenarioItem
+ Swarm.TUI.Model.Menu: modalDialog :: Lens' Modal (Dialog ButtonAction Name)
+ Swarm.TUI.Model.Menu: modalType :: Lens' Modal ModalType
+ Swarm.TUI.Model.Name: AchievementList :: Name
+ Swarm.TUI.Model.Name: Button :: Button -> Name
+ Swarm.TUI.Model.Name: CancelButton :: Button
+ Swarm.TUI.Model.Name: FocusablePanel :: FocusablePanel -> Name
+ Swarm.TUI.Model.Name: GoalSummary :: GoalWidget
+ Swarm.TUI.Model.Name: GoalWidgets :: GoalWidget -> Name
+ Swarm.TUI.Model.Name: InfoPanel :: FocusablePanel
+ Swarm.TUI.Model.Name: InfoViewport :: Name
+ Swarm.TUI.Model.Name: InventoryList :: Name
+ Swarm.TUI.Model.Name: InventoryListItem :: Int -> Name
+ Swarm.TUI.Model.Name: KeepPlayingButton :: Button
+ Swarm.TUI.Model.Name: MenuList :: Name
+ Swarm.TUI.Model.Name: ModalViewport :: Name
+ Swarm.TUI.Model.Name: NextButton :: Button
+ Swarm.TUI.Model.Name: ObjectivesList :: GoalWidget
+ Swarm.TUI.Model.Name: QuitButton :: Button
+ Swarm.TUI.Model.Name: REPLInput :: Name
+ Swarm.TUI.Model.Name: REPLPanel :: FocusablePanel
+ Swarm.TUI.Model.Name: RobotPanel :: FocusablePanel
+ Swarm.TUI.Model.Name: ScenarioList :: Name
+ Swarm.TUI.Model.Name: StartOverButton :: Button
+ Swarm.TUI.Model.Name: WorldCache :: Name
+ Swarm.TUI.Model.Name: WorldExtent :: Name
+ Swarm.TUI.Model.Name: WorldPanel :: FocusablePanel
+ Swarm.TUI.Model.Name: data Button
+ Swarm.TUI.Model.Name: data FocusablePanel
+ Swarm.TUI.Model.Name: data GoalWidget
+ Swarm.TUI.Model.Name: data Name
+ Swarm.TUI.Model.Name: instance GHC.Classes.Eq Swarm.TUI.Model.Name.Button
+ Swarm.TUI.Model.Name: instance GHC.Classes.Eq Swarm.TUI.Model.Name.FocusablePanel
+ Swarm.TUI.Model.Name: instance GHC.Classes.Eq Swarm.TUI.Model.Name.GoalWidget
+ Swarm.TUI.Model.Name: instance GHC.Classes.Eq Swarm.TUI.Model.Name.Name
+ Swarm.TUI.Model.Name: instance GHC.Classes.Ord Swarm.TUI.Model.Name.Button
+ Swarm.TUI.Model.Name: instance GHC.Classes.Ord Swarm.TUI.Model.Name.FocusablePanel
+ Swarm.TUI.Model.Name: instance GHC.Classes.Ord Swarm.TUI.Model.Name.GoalWidget
+ Swarm.TUI.Model.Name: instance GHC.Classes.Ord Swarm.TUI.Model.Name.Name
+ Swarm.TUI.Model.Name: instance GHC.Enum.Bounded Swarm.TUI.Model.Name.Button
+ Swarm.TUI.Model.Name: instance GHC.Enum.Bounded Swarm.TUI.Model.Name.FocusablePanel
+ Swarm.TUI.Model.Name: instance GHC.Enum.Bounded Swarm.TUI.Model.Name.GoalWidget
+ Swarm.TUI.Model.Name: instance GHC.Enum.Enum Swarm.TUI.Model.Name.Button
+ Swarm.TUI.Model.Name: instance GHC.Enum.Enum Swarm.TUI.Model.Name.FocusablePanel
+ Swarm.TUI.Model.Name: instance GHC.Enum.Enum Swarm.TUI.Model.Name.GoalWidget
+ Swarm.TUI.Model.Name: instance GHC.Read.Read Swarm.TUI.Model.Name.Button
+ Swarm.TUI.Model.Name: instance GHC.Read.Read Swarm.TUI.Model.Name.FocusablePanel
+ Swarm.TUI.Model.Name: instance GHC.Read.Read Swarm.TUI.Model.Name.GoalWidget
+ Swarm.TUI.Model.Name: instance GHC.Read.Read Swarm.TUI.Model.Name.Name
+ Swarm.TUI.Model.Name: instance GHC.Show.Show Swarm.TUI.Model.Name.Button
+ Swarm.TUI.Model.Name: instance GHC.Show.Show Swarm.TUI.Model.Name.FocusablePanel
+ Swarm.TUI.Model.Name: instance GHC.Show.Show Swarm.TUI.Model.Name.GoalWidget
+ Swarm.TUI.Model.Name: instance GHC.Show.Show Swarm.TUI.Model.Name.Name
+ Swarm.TUI.Model.Repl: CmdPrompt :: [Text] -> REPLPrompt
+ Swarm.TUI.Model.Repl: Newer :: TimeDir
+ Swarm.TUI.Model.Repl: Older :: TimeDir
+ Swarm.TUI.Model.Repl: Piloting :: ReplControlMode
+ Swarm.TUI.Model.Repl: REPLEntry :: Text -> REPLHistItem
+ Swarm.TUI.Model.Repl: REPLOutput :: Text -> REPLHistItem
+ Swarm.TUI.Model.Repl: SearchPrompt :: REPLHistory -> REPLPrompt
+ Swarm.TUI.Model.Repl: Typing :: ReplControlMode
+ Swarm.TUI.Model.Repl: addREPLItem :: REPLHistItem -> REPLHistory -> REPLHistory
+ Swarm.TUI.Model.Repl: data REPLHistItem
+ Swarm.TUI.Model.Repl: data REPLHistory
+ Swarm.TUI.Model.Repl: data REPLPrompt
+ Swarm.TUI.Model.Repl: data REPLState
+ Swarm.TUI.Model.Repl: data ReplControlMode
+ Swarm.TUI.Model.Repl: data TimeDir
+ Swarm.TUI.Model.Repl: defaultPrompt :: REPLPrompt
+ Swarm.TUI.Model.Repl: getCurrentItemText :: REPLHistory -> Maybe Text
+ Swarm.TUI.Model.Repl: getLatestREPLHistoryItems :: Int -> REPLHistory -> [REPLHistItem]
+ Swarm.TUI.Model.Repl: getREPLEntry :: REPLHistItem -> Maybe Text
+ Swarm.TUI.Model.Repl: initREPLState :: REPLHistory -> REPLState
+ Swarm.TUI.Model.Repl: instance GHC.Classes.Eq Swarm.TUI.Model.Repl.REPLHistItem
+ Swarm.TUI.Model.Repl: instance GHC.Classes.Eq Swarm.TUI.Model.Repl.ReplControlMode
+ Swarm.TUI.Model.Repl: instance GHC.Classes.Eq Swarm.TUI.Model.Repl.TimeDir
+ Swarm.TUI.Model.Repl: instance GHC.Classes.Ord Swarm.TUI.Model.Repl.REPLHistItem
+ Swarm.TUI.Model.Repl: instance GHC.Classes.Ord Swarm.TUI.Model.Repl.TimeDir
+ Swarm.TUI.Model.Repl: instance GHC.Enum.Bounded Swarm.TUI.Model.Repl.ReplControlMode
+ Swarm.TUI.Model.Repl: instance GHC.Enum.Enum Swarm.TUI.Model.Repl.ReplControlMode
+ Swarm.TUI.Model.Repl: instance GHC.Read.Read Swarm.TUI.Model.Repl.REPLHistItem
+ Swarm.TUI.Model.Repl: instance GHC.Show.Show Swarm.TUI.Model.Repl.REPLHistItem
+ Swarm.TUI.Model.Repl: instance GHC.Show.Show Swarm.TUI.Model.Repl.TimeDir
+ Swarm.TUI.Model.Repl: isREPLEntry :: REPLHistItem -> Bool
+ Swarm.TUI.Model.Repl: lastEntry :: Text -> REPLHistory -> Maybe Text
+ Swarm.TUI.Model.Repl: moveReplHistIndex :: TimeDir -> Text -> REPLHistory -> REPLHistory
+ Swarm.TUI.Model.Repl: newREPLEditor :: Text -> Editor Text Name
+ Swarm.TUI.Model.Repl: newREPLHistory :: [REPLHistItem] -> REPLHistory
+ Swarm.TUI.Model.Repl: removeEntry :: Text -> REPLHistory -> REPLHistory
+ Swarm.TUI.Model.Repl: replControlMode :: Lens' REPLState ReplControlMode
+ Swarm.TUI.Model.Repl: replHistory :: Lens' REPLState REPLHistory
+ Swarm.TUI.Model.Repl: replIndex :: Lens' REPLHistory Int
+ Swarm.TUI.Model.Repl: replIndexIsAtInput :: REPLHistory -> Bool
+ Swarm.TUI.Model.Repl: replItemText :: REPLHistItem -> Text
+ Swarm.TUI.Model.Repl: replLast :: Lens' REPLState Text
+ Swarm.TUI.Model.Repl: replLength :: REPLHistory -> Int
+ Swarm.TUI.Model.Repl: replPromptEditor :: Lens' REPLState (Editor Text Name)
+ Swarm.TUI.Model.Repl: replPromptText :: Lens' REPLState Text
+ Swarm.TUI.Model.Repl: replPromptType :: Lens' REPLState REPLPrompt
+ Swarm.TUI.Model.Repl: replSeq :: Lens' REPLHistory (Seq REPLHistItem)
+ Swarm.TUI.Model.Repl: replType :: Lens' REPLState (Maybe Polytype)
+ Swarm.TUI.Model.Repl: replValid :: Lens' REPLState Bool
+ Swarm.TUI.Model.Repl: restartREPLHistory :: REPLHistory -> REPLHistory
+ Swarm.TUI.Model.StateUpdate: attainAchievement :: (MonadIO m, MonadState AppState m) => CategorizedAchievement -> m ()
+ Swarm.TUI.Model.StateUpdate: attainAchievement' :: (MonadIO m, MonadState AppState m) => ZonedTime -> Maybe FilePath -> CategorizedAchievement -> m ()
+ Swarm.TUI.Model.StateUpdate: initAppState :: AppOpts -> ExceptT Text IO AppState
+ Swarm.TUI.Model.StateUpdate: restartGame :: (MonadIO m, MonadState AppState m) => Seed -> ScenarioInfoPair -> m ()
+ Swarm.TUI.Model.StateUpdate: scenarioToAppState :: (MonadIO m, MonadState AppState m) => ScenarioInfoPair -> Maybe Seed -> Maybe CodeToRun -> m ()
+ Swarm.TUI.Model.StateUpdate: startGame :: (MonadIO m, MonadState AppState m) => ScenarioInfoPair -> Maybe CodeToRun -> m ()
+ Swarm.TUI.Model.UI: GoalDisplay :: GoalTracking -> List Name GoalEntry -> FocusRing Name -> GoalDisplay
+ Swarm.TUI.Model.UI: UIState :: Menu -> Bool -> Bool -> FocusRing Name -> Maybe Coords -> REPLState -> Maybe (Int, List Name InventoryListEntry) -> InventorySortOptions -> Bool -> Bool -> Bool -> Maybe Text -> Maybe Modal -> GoalDisplay -> Map CategorizedAchievement Attainment -> Bool -> Bool -> TimeSpec -> Bool -> Double -> Double -> Int -> Int -> Int -> Int -> TimeSpec -> TimeSpec -> TimeSpec -> Map Text Text -> AttrMap -> Maybe ScenarioInfoPair -> UIState
+ Swarm.TUI.Model.UI: [_accumulatedTime] :: UIState -> TimeSpec
+ Swarm.TUI.Model.UI: [_appData] :: UIState -> Map Text Text
+ Swarm.TUI.Model.UI: [_focus] :: GoalDisplay -> FocusRing Name
+ Swarm.TUI.Model.UI: [_frameCount] :: UIState -> Int
+ Swarm.TUI.Model.UI: [_frameTickCount] :: UIState -> Int
+ Swarm.TUI.Model.UI: [_goalsContent] :: GoalDisplay -> GoalTracking
+ Swarm.TUI.Model.UI: [_lastFrameTime] :: UIState -> TimeSpec
+ Swarm.TUI.Model.UI: [_lastInfoTime] :: UIState -> TimeSpec
+ Swarm.TUI.Model.UI: [_lgTicksPerSecond] :: UIState -> Int
+ Swarm.TUI.Model.UI: [_listWidget] :: GoalDisplay -> List Name GoalEntry
+ Swarm.TUI.Model.UI: [_scenarioRef] :: UIState -> Maybe ScenarioInfoPair
+ Swarm.TUI.Model.UI: [_tickCount] :: UIState -> Int
+ Swarm.TUI.Model.UI: [_uiAchievements] :: UIState -> Map CategorizedAchievement Attainment
+ Swarm.TUI.Model.UI: [_uiAttrMap] :: UIState -> AttrMap
+ Swarm.TUI.Model.UI: [_uiCheatMode] :: UIState -> Bool
+ Swarm.TUI.Model.UI: [_uiError] :: UIState -> Maybe Text
+ Swarm.TUI.Model.UI: [_uiFPS] :: UIState -> Double
+ Swarm.TUI.Model.UI: [_uiFocusRing] :: UIState -> FocusRing Name
+ Swarm.TUI.Model.UI: [_uiGoal] :: UIState -> GoalDisplay
+ Swarm.TUI.Model.UI: [_uiHideRobotsUntil] :: UIState -> TimeSpec
+ Swarm.TUI.Model.UI: [_uiInventoryShouldUpdate] :: UIState -> Bool
+ Swarm.TUI.Model.UI: [_uiInventorySort] :: UIState -> InventorySortOptions
+ Swarm.TUI.Model.UI: [_uiInventory] :: UIState -> Maybe (Int, List Name InventoryListEntry)
+ Swarm.TUI.Model.UI: [_uiMenu] :: UIState -> Menu
+ Swarm.TUI.Model.UI: [_uiModal] :: UIState -> Maybe Modal
+ Swarm.TUI.Model.UI: [_uiMoreInfoBot] :: UIState -> Bool
+ Swarm.TUI.Model.UI: [_uiMoreInfoTop] :: UIState -> Bool
+ Swarm.TUI.Model.UI: [_uiPlaying] :: UIState -> Bool
+ Swarm.TUI.Model.UI: [_uiREPL] :: UIState -> REPLState
+ Swarm.TUI.Model.UI: [_uiScrollToEnd] :: UIState -> Bool
+ Swarm.TUI.Model.UI: [_uiShowFPS] :: UIState -> Bool
+ Swarm.TUI.Model.UI: [_uiShowZero] :: UIState -> Bool
+ Swarm.TUI.Model.UI: [_uiTPF] :: UIState -> Double
+ Swarm.TUI.Model.UI: [_uiWorldCursor] :: UIState -> Maybe Coords
+ Swarm.TUI.Model.UI: accumulatedTime :: Lens' UIState TimeSpec
+ Swarm.TUI.Model.UI: appData :: Lens' UIState (Map Text Text)
+ Swarm.TUI.Model.UI: data GoalDisplay
+ Swarm.TUI.Model.UI: data UIState
+ Swarm.TUI.Model.UI: frameCount :: Lens' UIState Int
+ Swarm.TUI.Model.UI: frameTickCount :: Lens' UIState Int
+ Swarm.TUI.Model.UI: initFocusRing :: FocusRing Name
+ Swarm.TUI.Model.UI: initLgTicksPerSecond :: Int
+ Swarm.TUI.Model.UI: initUIState :: Bool -> Bool -> ExceptT Text IO ([SystemFailure], UIState)
+ Swarm.TUI.Model.UI: lastFrameTime :: Lens' UIState TimeSpec
+ Swarm.TUI.Model.UI: lastInfoTime :: Lens' UIState TimeSpec
+ Swarm.TUI.Model.UI: lgTicksPerSecond :: Lens' UIState Int
+ Swarm.TUI.Model.UI: scenarioRef :: Lens' UIState (Maybe ScenarioInfoPair)
+ Swarm.TUI.Model.UI: tickCount :: Lens' UIState Int
+ Swarm.TUI.Model.UI: uiAchievements :: Lens' UIState (Map CategorizedAchievement Attainment)
+ Swarm.TUI.Model.UI: uiAttrMap :: Lens' UIState AttrMap
+ Swarm.TUI.Model.UI: uiCheatMode :: Lens' UIState Bool
+ Swarm.TUI.Model.UI: uiError :: Lens' UIState (Maybe Text)
+ Swarm.TUI.Model.UI: uiFPS :: Lens' UIState Double
+ Swarm.TUI.Model.UI: uiFocusRing :: Lens' UIState (FocusRing Name)
+ Swarm.TUI.Model.UI: uiGoal :: Lens' UIState GoalDisplay
+ Swarm.TUI.Model.UI: uiHideRobotsUntil :: Lens' UIState TimeSpec
+ Swarm.TUI.Model.UI: uiInventory :: Lens' UIState (Maybe (Int, List Name InventoryListEntry))
+ Swarm.TUI.Model.UI: uiInventoryShouldUpdate :: Lens' UIState Bool
+ Swarm.TUI.Model.UI: uiInventorySort :: Lens' UIState InventorySortOptions
+ Swarm.TUI.Model.UI: uiMenu :: Lens' UIState Menu
+ Swarm.TUI.Model.UI: uiModal :: Lens' UIState (Maybe Modal)
+ Swarm.TUI.Model.UI: uiMoreInfoBot :: Lens' UIState Bool
+ Swarm.TUI.Model.UI: uiMoreInfoTop :: Lens' UIState Bool
+ Swarm.TUI.Model.UI: uiPlaying :: Lens' UIState Bool
+ Swarm.TUI.Model.UI: uiREPL :: Lens' UIState REPLState
+ Swarm.TUI.Model.UI: uiScrollToEnd :: Lens' UIState Bool
+ Swarm.TUI.Model.UI: uiShowFPS :: Lens' UIState Bool
+ Swarm.TUI.Model.UI: uiShowRobots :: Getter UIState Bool
+ Swarm.TUI.Model.UI: uiShowZero :: Lens' UIState Bool
+ Swarm.TUI.Model.UI: uiTPF :: Lens' UIState Double
+ Swarm.TUI.Model.UI: uiWorldCursor :: Lens' UIState (Maybe Coords)
+ Swarm.TUI.View.Achievement: drawAchievementListItem :: Map CategorizedAchievement Attainment -> CategorizedAchievement -> Widget Name
+ Swarm.TUI.View.Achievement: drawAchievementsMenuUI :: AppState -> List Name CategorizedAchievement -> Widget Name
+ Swarm.TUI.View.Achievement: getCompletionIcon :: Bool -> Widget Name
+ Swarm.TUI.View.Achievement: padAllEvenly :: Int -> Widget Name -> Widget Name
+ Swarm.TUI.View.Achievement: singleAchievementDetails :: Map CategorizedAchievement Attainment -> CategorizedAchievement -> Widget Name
+ Swarm.TUI.View.CellDisplay: HideAllEntities :: HideEntity
+ Swarm.TUI.View.CellDisplay: HideEntityUnknownTo :: Robot -> HideEntity
+ Swarm.TUI.View.CellDisplay: HideNoEntity :: HideEntity
+ Swarm.TUI.View.CellDisplay: data HideEntity
+ Swarm.TUI.View.CellDisplay: displayEntityCell :: GameState -> Coords -> [Display]
+ Swarm.TUI.View.CellDisplay: displayLoc :: Bool -> GameState -> Coords -> Display
+ Swarm.TUI.View.CellDisplay: displayRobotCell :: GameState -> Coords -> [Display]
+ Swarm.TUI.View.CellDisplay: displayTerrainCell :: GameState -> Coords -> Display
+ Swarm.TUI.View.CellDisplay: drawLoc :: Bool -> GameState -> Coords -> Widget Name
+ Swarm.TUI.View.CellDisplay: hidingMode :: GameState -> HideEntity
+ Swarm.TUI.View.CustomStyling: toAttrColor :: HexColor -> Color
+ Swarm.TUI.View.CustomStyling: toAttrPair :: CustomAttr -> (AttrName, Attr)
+ Swarm.TUI.View.CustomStyling: toStyle :: StyleFlag -> Style
+ Swarm.TUI.View.Util: curMenuName :: AppState -> Maybe Text
+ Swarm.TUI.View.Util: descriptionTitle :: Entity -> String
+ Swarm.TUI.View.Util: displayParagraphs :: [Text] -> Widget Name
+ Swarm.TUI.View.Util: drawType :: Polytype -> Widget Name
+ Swarm.TUI.View.Util: generateModal :: AppState -> ModalType -> Modal
+ Swarm.TUI.View.Util: maxModalWindowWidth :: Int
+ Swarm.TUI.View.Util: maybeScroll :: (Ord n, Show n) => n -> Widget n -> Widget n
+ Swarm.TUI.View.Util: quitMsg :: Menu -> Text
+ Swarm.TUI.View.Util: withEllipsis :: Text -> Widget Name
+ Swarm.Util: binTuples :: (Foldable t, Ord a) => t (a, b) -> Map a (NonEmpty b)
+ Swarm.Util: bquote :: Text -> Text
+ Swarm.Util: getSwarmXdgDataFile :: Bool -> FilePath -> IO FilePath
+ Swarm.Util: getSwarmXdgDataSubdir :: Bool -> FilePath -> IO FilePath
+ Swarm.Util: listEnums :: (Enum e, Bounded e) => [e]
+ Swarm.Util.Location: (.+^) :: (Affine p, Num a) => p a -> Diff p a -> p a
+ Swarm.Util.Location: (.-.) :: (Affine p, Num a) => p a -> p a -> Diff p a
+ Swarm.Util.Location: (.-^) :: (Affine p, Num a) => p a -> Diff p a -> p a
+ Swarm.Util.Location: class Additive Diff p => Affine (p :: Type -> Type) where {
+ Swarm.Util.Location: infixl 6 .-^
+ Swarm.Util.Location: instance Data.Aeson.Types.FromJSON.FromJSON (Linear.V2.V2 GHC.Int.Int32)
+ Swarm.Util.Location: instance Data.Aeson.Types.FromJSON.FromJSON Swarm.Util.Location.Location
+ Swarm.Util.Location: instance Data.Aeson.Types.FromJSON.FromJSONKey (Linear.V2.V2 GHC.Int.Int32)
+ Swarm.Util.Location: instance Data.Aeson.Types.ToJSON.ToJSON (Linear.V2.V2 GHC.Int.Int32)
+ Swarm.Util.Location: instance Data.Aeson.Types.ToJSON.ToJSON Swarm.Util.Location.Location
+ Swarm.Util.Location: instance Data.Aeson.Types.ToJSON.ToJSONKey (Linear.V2.V2 GHC.Int.Int32)
+ Swarm.Util.Location: origin :: forall (f :: Type -> Type) a. (Additive f, Num a) => Point f a
+ Swarm.Util.Location: pattern Location :: Int32 -> Int32 -> Location
+ Swarm.Util.Location: type Heading = V2 Int32
+ Swarm.Util.Location: type Location = Point V2 Int32
+ Swarm.Util.Location: type family Diff (p :: Type -> Type) :: Type -> Type;
+ Swarm.Util.Location: }
- Swarm.Game.CESK: FImmediate :: WorldUpdate -> RobotUpdate -> Frame
+ Swarm.Game.CESK: FImmediate :: Const -> [WorldUpdate Entity] -> [RobotUpdate] -> Frame
- Swarm.Game.CESK: FUpdate :: Loc -> Frame
+ Swarm.Game.CESK: FUpdate :: Addr -> Frame
- Swarm.Game.CESK: allocate :: Env -> Term -> Store -> (Loc, Store)
+ Swarm.Game.CESK: allocate :: Env -> Term -> Store -> (Addr, Store)
- Swarm.Game.CESK: lookupCell :: Loc -> Store -> Maybe Cell
+ Swarm.Game.CESK: lookupCell :: Addr -> Store -> Maybe Cell
- Swarm.Game.CESK: setCell :: Loc -> Cell -> Store -> Store
+ Swarm.Game.CESK: setCell :: Addr -> Cell -> Store -> Store
- Swarm.Game.Display: orientationMap :: Lens' Display (Map Direction Char)
+ Swarm.Game.Display: orientationMap :: Lens' Display (Map AbsoluteDir Char)
- Swarm.Game.Entity: entityCapabilities :: Lens' Entity [Capability]
+ Swarm.Game.Entity: entityCapabilities :: Lens' Entity (Set Capability)
- Swarm.Game.Entity: entityOrientation :: Lens' Entity (Maybe (V2 Int64))
+ Swarm.Game.Entity: entityOrientation :: Lens' Entity (Maybe Heading)
- Swarm.Game.Exception: CmdFailed :: Const -> Text -> Exn
+ Swarm.Game.Exception: CmdFailed :: Const -> Text -> Maybe GameplayAchievement -> Exn
- Swarm.Game.Recipe: make :: (Inventory, Inventory) -> Recipe Entity -> Either [MissingIngredient] (Inventory, Inventory -> Inventory, Recipe Entity)
+ Swarm.Game.Recipe: make :: (Inventory, Inventory) -> Recipe Entity -> Either [MissingIngredient] (Inventory, IngredientList Entity, Recipe Entity)
- Swarm.Game.Robot: mkRobot :: RobotID phase -> Maybe Int -> Text -> [Text] -> RobotLocation phase -> V2 Int64 -> Display -> CESK -> [Entity] -> [(Count, Entity)] -> Bool -> Bool -> TimeSpec -> RobotR phase
+ Swarm.Game.Robot: mkRobot :: RobotID phase -> Maybe Int -> Text -> [Text] -> RobotLocation phase -> Heading -> Display -> CESK -> [Entity] -> [(Count, Entity)] -> Bool -> Bool -> TimeSpec -> RobotR phase
- Swarm.Game.Robot: robotLocation :: Getter Robot (V2 Int64)
+ Swarm.Game.Robot: robotLocation :: Getter Robot Location
- Swarm.Game.Robot: robotOrientation :: Lens' Robot (Maybe (V2 Int64))
+ Swarm.Game.Robot: robotOrientation :: Lens' Robot (Maybe Heading)
- Swarm.Game.Robot: trobotLocation :: Lens' TRobot (Maybe (V2 Int64))
+ Swarm.Game.Robot: trobotLocation :: Lens' TRobot (Maybe Location)
- Swarm.Game.Robot: unsafeSetRobotLocation :: V2 Int64 -> Robot -> Robot
+ Swarm.Game.Robot: unsafeSetRobotLocation :: Location -> Robot -> Robot
- Swarm.Game.Scenario: Cell :: TerrainType -> Maybe Entity -> [IndexedTRobot] -> Cell
+ Swarm.Game.Scenario: Cell :: TerrainType -> Maybe e -> [IndexedTRobot] -> PCell e
- Swarm.Game.Scenario: WorldDescription :: Maybe Cell -> Bool -> WorldPalette -> V2 Int64 -> [[Cell]] -> WorldDescription
+ Swarm.Game.Scenario: WorldDescription :: Maybe (PCell e) -> Bool -> WorldPalette e -> Location -> [[PCell e]] -> PWorldDescription e
- Swarm.Game.Scenario: [area] :: WorldDescription -> [[Cell]]
+ Swarm.Game.Scenario: [area] :: PWorldDescription e -> [[PCell e]]
- Swarm.Game.Scenario: [cellEntity] :: Cell -> Maybe Entity
+ Swarm.Game.Scenario: [cellEntity] :: PCell e -> Maybe e
- Swarm.Game.Scenario: [cellRobots] :: Cell -> [IndexedTRobot]
+ Swarm.Game.Scenario: [cellRobots] :: PCell e -> [IndexedTRobot]
- Swarm.Game.Scenario: [cellTerrain] :: Cell -> TerrainType
+ Swarm.Game.Scenario: [cellTerrain] :: PCell e -> TerrainType
- Swarm.Game.Scenario: [defaultTerrain] :: WorldDescription -> Maybe Cell
+ Swarm.Game.Scenario: [defaultTerrain] :: PWorldDescription e -> Maybe (PCell e)
- Swarm.Game.Scenario: [offsetOrigin] :: WorldDescription -> Bool
+ Swarm.Game.Scenario: [offsetOrigin] :: PWorldDescription e -> Bool
- Swarm.Game.Scenario: [palette] :: WorldDescription -> WorldPalette
+ Swarm.Game.Scenario: [palette] :: PWorldDescription e -> WorldPalette e
- Swarm.Game.Scenario: [ul] :: WorldDescription -> V2 Int64
+ Swarm.Game.Scenario: [ul] :: PWorldDescription e -> Location
- Swarm.Game.State: VCLocation :: V2 Int64 -> ViewCenterRule
+ Swarm.Game.State: VCLocation :: Location -> ViewCenterRule
- Swarm.Game.State: WinConditions :: NonEmpty Objective -> WinCondition
+ Swarm.Game.State: WinConditions :: WinStatus -> ObjectiveCompletion -> WinCondition
- Swarm.Game.State: Won :: Bool -> WinCondition
+ Swarm.Game.State: Won :: Bool -> WinStatus
- Swarm.Game.State: _WinConditions :: Prism' WinCondition (NonEmpty Objective)
+ Swarm.Game.State: _WinConditions :: Prism' WinCondition (WinStatus, ObjectiveCompletion)
- Swarm.Game.State: applyViewCenterRule :: ViewCenterRule -> IntMap Robot -> Maybe (V2 Int64)
+ Swarm.Game.State: applyViewCenterRule :: ViewCenterRule -> IntMap Robot -> Maybe Location
- Swarm.Game.State: messageIsFromNearby :: V2 Int64 -> LogEntry -> Bool
+ Swarm.Game.State: messageIsFromNearby :: Location -> LogEntry -> Bool
- Swarm.Game.State: modifyViewCenter :: (V2 Int64 -> V2 Int64) -> GameState -> GameState
+ Swarm.Game.State: modifyViewCenter :: (Location -> Location) -> GameState -> GameState
- Swarm.Game.State: notificationsContent :: forall a_a4l8d a_a4lBr. Lens (Notifications a_a4l8d) (Notifications a_a4lBr) [a_a4l8d] [a_a4lBr]
+ Swarm.Game.State: notificationsContent :: forall a_a5PKt a_a5QcT. Lens (Notifications a_a5PKt) (Notifications a_a5QcT) [a_a5PKt] [a_a5QcT]
- Swarm.Game.State: notificationsCount :: forall a_a4l8d. Lens' (Notifications a_a4l8d) Int
+ Swarm.Game.State: notificationsCount :: forall a_a5PKt. Lens' (Notifications a_a5PKt) Int
- Swarm.Game.State: robotsAtLocation :: V2 Int64 -> GameState -> [Robot]
+ Swarm.Game.State: robotsAtLocation :: Location -> GameState -> [Robot]
- Swarm.Game.State: robotsByLocation :: Lens' GameState (Map (V2 Int64) IntSet)
+ Swarm.Game.State: robotsByLocation :: Lens' GameState (Map Location IntSet)
- Swarm.Game.State: robotsInArea :: V2 Int64 -> Int64 -> GameState -> [Robot]
+ Swarm.Game.State: robotsInArea :: Location -> Int32 -> GameState -> [Robot]
- Swarm.Game.State: viewCenter :: Getter GameState (V2 Int64)
+ Swarm.Game.State: viewCenter :: Getter GameState Location
- Swarm.Game.State: viewingRegion :: GameState -> (Int64, Int64) -> (Coords, Coords)
+ Swarm.Game.State: viewingRegion :: GameState -> (Int32, Int32) -> (Coords, Coords)
- Swarm.Game.Step: addSeedBot :: Has (State GameState) sig m => Entity -> (Integer, Integer) -> V2 Int64 -> TimeSpec -> m ()
+ Swarm.Game.Step: addSeedBot :: Has (State GameState) sig m => Entity -> (Integer, Integer) -> Location -> TimeSpec -> m ()
- Swarm.Game.Step: entityAt :: Has (State GameState) sig m => V2 Int64 -> m (Maybe Entity)
+ Swarm.Game.Step: entityAt :: Has (State GameState) sig m => Location -> m (Maybe Entity)
- Swarm.Game.Step: updateEntityAt :: Has (State GameState) sig m => V2 Int64 -> (Maybe Entity -> Maybe Entity) -> m ()
+ Swarm.Game.Step: updateEntityAt :: Has (State GameState) sig m => Location -> (Maybe Entity -> Maybe Entity) -> m ()
- Swarm.Game.Step: updateRobotLocation :: HasRobotStepState sig m => V2 Int64 -> V2 Int64 -> m ()
+ Swarm.Game.Step: updateRobotLocation :: HasRobotStepState sig m => Location -> Location -> m ()
- Swarm.Game.World: Coords :: (Int64, Int64) -> Coords
+ Swarm.Game.World: Coords :: (Int32, Int32) -> Coords
- Swarm.Game.World: [unCoords] :: Coords -> (Int64, Int64)
+ Swarm.Game.World: [unCoords] :: Coords -> (Int32, Int32)
- Swarm.Game.World: coordsToLoc :: Coords -> V2 Int64
+ Swarm.Game.World: coordsToLoc :: Coords -> Location
- Swarm.Game.World: locToCoords :: V2 Int64 -> Coords
+ Swarm.Game.World: locToCoords :: Location -> Coords
- Swarm.Game.World: worldFunFromArray :: Array (Int64, Int64) (t, Maybe e) -> (t, Maybe e) -> WorldFun t e
+ Swarm.Game.World: worldFunFromArray :: Array (Int32, Int32) (t, Maybe e) -> (t, Maybe e) -> WorldFun t e
- Swarm.Game.WorldGen: testWorld2FromArray :: EntityMap -> Array (Int64, Int64) (TerrainType, Maybe Entity) -> Seed -> WorldFun TerrainType Entity
+ Swarm.Game.WorldGen: testWorld2FromArray :: EntityMap -> Array (Int32, Int32) (TerrainType, Maybe Entity) -> Seed -> WorldFun TerrainType Entity
- Swarm.Language.Elaborate: elaborate :: Term -> Term
+ Swarm.Language.Elaborate: elaborate :: Syntax' Polytype -> Syntax' Polytype
- Swarm.Language.Elaborate: wrapForce :: Var -> Term -> Term
+ Swarm.Language.Elaborate: wrapForce :: Var -> Syntax' Polytype -> Syntax' Polytype
- Swarm.Language.Pipeline: ProcessedTerm :: Term -> TModule -> Requirements -> ReqCtx -> ProcessedTerm
+ Swarm.Language.Pipeline: ProcessedTerm :: TModule -> Requirements -> ReqCtx -> ProcessedTerm
- Swarm.Language.Syntax: DBack :: Direction
+ Swarm.Language.Syntax: DBack :: RelativeDir
- Swarm.Language.Syntax: DDown :: Direction
+ Swarm.Language.Syntax: DDown :: RelativeDir
- Swarm.Language.Syntax: DEast :: Direction
+ Swarm.Language.Syntax: DEast :: AbsoluteDir
- Swarm.Language.Syntax: DForward :: Direction
+ Swarm.Language.Syntax: DForward :: RelativeDir
- Swarm.Language.Syntax: DLeft :: Direction
+ Swarm.Language.Syntax: DLeft :: RelativeDir
- Swarm.Language.Syntax: DNorth :: Direction
+ Swarm.Language.Syntax: DNorth :: AbsoluteDir
- Swarm.Language.Syntax: DRight :: Direction
+ Swarm.Language.Syntax: DRight :: RelativeDir
- Swarm.Language.Syntax: DSouth :: Direction
+ Swarm.Language.Syntax: DSouth :: AbsoluteDir
- Swarm.Language.Syntax: DWest :: Direction
+ Swarm.Language.Syntax: DWest :: AbsoluteDir
- Swarm.Language.Syntax: DirInfo :: Text -> Maybe (V2 Int64) -> (V2 Int64 -> V2 Int64) -> DirInfo
+ Swarm.Language.Syntax: DirInfo :: Text -> (Heading -> Heading) -> DirInfo
- Swarm.Language.Syntax: NoLoc :: Location
+ Swarm.Language.Syntax: NoLoc :: SrcLoc
- Swarm.Language.Syntax: SApp :: Syntax -> Syntax -> Term
+ Swarm.Language.Syntax: SApp :: Syntax' ty -> Syntax' ty -> Term' ty
- Swarm.Language.Syntax: SBind :: Maybe Var -> Syntax -> Syntax -> Term
+ Swarm.Language.Syntax: SBind :: Maybe LocVar -> Syntax' ty -> Syntax' ty -> Term' ty
- Swarm.Language.Syntax: SDef :: Bool -> Var -> Maybe Polytype -> Syntax -> Term
+ Swarm.Language.Syntax: SDef :: Bool -> LocVar -> Maybe Polytype -> Syntax' ty -> Term' ty
- Swarm.Language.Syntax: SDelay :: DelayType -> Syntax -> Term
+ Swarm.Language.Syntax: SDelay :: DelayType -> Syntax' ty -> Term' ty
- Swarm.Language.Syntax: SLam :: Var -> Maybe Type -> Syntax -> Term
+ Swarm.Language.Syntax: SLam :: LocVar -> Maybe Type -> Syntax' ty -> Term' ty
- Swarm.Language.Syntax: SLet :: Bool -> Var -> Maybe Polytype -> Syntax -> Syntax -> Term
+ Swarm.Language.Syntax: SLet :: Bool -> LocVar -> Maybe Polytype -> Syntax' ty -> Syntax' ty -> Term' ty
- Swarm.Language.Syntax: SPair :: Syntax -> Syntax -> Term
+ Swarm.Language.Syntax: SPair :: Syntax' ty -> Syntax' ty -> Term' ty
- Swarm.Language.Syntax: TAntiInt :: Text -> Term
+ Swarm.Language.Syntax: TAntiInt :: Text -> Term' ty
- Swarm.Language.Syntax: TAntiText :: Text -> Term
+ Swarm.Language.Syntax: TAntiText :: Text -> Term' ty
- Swarm.Language.Syntax: TBool :: Bool -> Term
+ Swarm.Language.Syntax: TBool :: Bool -> Term' ty
- Swarm.Language.Syntax: TConst :: Const -> Term
+ Swarm.Language.Syntax: TConst :: Const -> Term' ty
- Swarm.Language.Syntax: TDir :: Direction -> Term
+ Swarm.Language.Syntax: TDir :: Direction -> Term' ty
- Swarm.Language.Syntax: TInt :: Integer -> Term
+ Swarm.Language.Syntax: TInt :: Integer -> Term' ty
- Swarm.Language.Syntax: TRef :: Int -> Term
+ Swarm.Language.Syntax: TRef :: Int -> Term' ty
- Swarm.Language.Syntax: TRequire :: Int -> Text -> Term
+ Swarm.Language.Syntax: TRequire :: Int -> Text -> Term' ty
- Swarm.Language.Syntax: TRequireDevice :: Text -> Term
+ Swarm.Language.Syntax: TRequireDevice :: Text -> Term' ty
- Swarm.Language.Syntax: TRobot :: Int -> Term
+ Swarm.Language.Syntax: TRobot :: Int -> Term' ty
- Swarm.Language.Syntax: TText :: Text -> Term
+ Swarm.Language.Syntax: TText :: Text -> Term' ty
- Swarm.Language.Syntax: TUnit :: Term
+ Swarm.Language.Syntax: TUnit :: Term' ty
- Swarm.Language.Syntax: TVar :: Var -> Term
+ Swarm.Language.Syntax: TVar :: Var -> Term' ty
- Swarm.Language.Syntax: [dirApplyTurn] :: DirInfo -> V2 Int64 -> V2 Int64
+ Swarm.Language.Syntax: [dirApplyTurn] :: DirInfo -> Heading -> Heading
- Swarm.Language.Syntax: applyTurn :: Direction -> V2 Int64 -> V2 Int64
+ Swarm.Language.Syntax: applyTurn :: Direction -> Heading -> Heading
- Swarm.Language.Syntax: east :: V2 Int64
+ Swarm.Language.Syntax: east :: Heading
- Swarm.Language.Syntax: fromDirection :: Direction -> V2 Int64
+ Swarm.Language.Syntax: fromDirection :: Direction -> Heading
- Swarm.Language.Syntax: north :: V2 Int64
+ Swarm.Language.Syntax: north :: Heading
- Swarm.Language.Syntax: south :: V2 Int64
+ Swarm.Language.Syntax: south :: Heading
- Swarm.Language.Syntax: toDirection :: V2 Int64 -> Maybe Direction
+ Swarm.Language.Syntax: toDirection :: Heading -> Maybe Direction
- Swarm.Language.Syntax: west :: V2 Int64
+ Swarm.Language.Syntax: west :: Heading
- Swarm.Language.Typecheck: (=:=) :: UType -> UType -> Infer ()
+ Swarm.Language.Typecheck: (=:=) :: UType -> UType -> Infer UType
- Swarm.Language.Typecheck: CantInfer :: Location -> Term -> TypeErr
+ Swarm.Language.Typecheck: CantInfer :: SrcLoc -> Term -> TypeErr
- Swarm.Language.Typecheck: DefNotTopLevel :: Location -> Term -> TypeErr
+ Swarm.Language.Typecheck: DefNotTopLevel :: SrcLoc -> Term -> TypeErr
- Swarm.Language.Typecheck: EscapedSkolem :: Location -> Var -> TypeErr
+ Swarm.Language.Typecheck: EscapedSkolem :: SrcLoc -> Var -> TypeErr
- Swarm.Language.Typecheck: InvalidAtomic :: Location -> InvalidAtomicReason -> Term -> TypeErr
+ Swarm.Language.Typecheck: InvalidAtomic :: SrcLoc -> InvalidAtomicReason -> Term -> TypeErr
- Swarm.Language.Typecheck: Mismatch :: Location -> TypeF UType -> TypeF UType -> TypeErr
+ Swarm.Language.Typecheck: Mismatch :: SrcLoc -> TypeF UType -> TypeF UType -> TypeErr
- Swarm.Language.Typecheck: UnboundVar :: Location -> Var -> TypeErr
+ Swarm.Language.Typecheck: UnboundVar :: SrcLoc -> Var -> TypeErr
- Swarm.Language.Typecheck: check :: Syntax -> UType -> Infer ()
+ Swarm.Language.Typecheck: check :: Syntax -> UType -> Infer (Syntax' UType)
- Swarm.Language.Typecheck: infer :: Syntax -> Infer UType
+ Swarm.Language.Typecheck: infer :: Syntax -> Infer (Syntax' UType)
- Swarm.Language.Typecheck: lookup :: Location -> Var -> Infer UType
+ Swarm.Language.Typecheck: lookup :: SrcLoc -> Var -> Infer UType
- Swarm.Language.Typed: polytype :: forall v_a295J. Lens' (Typed v_a295J) Polytype
+ Swarm.Language.Typed: polytype :: forall v_a3EKy. Lens' (Typed v_a3EKy) Polytype
- Swarm.Language.Typed: requires :: forall v_a295J. Lens' (Typed v_a295J) Requirements
+ Swarm.Language.Typed: requires :: forall v_a3EKy. Lens' (Typed v_a3EKy) Requirements
- Swarm.Language.Typed: value :: forall v_a295J v_a29lp. Lens (Typed v_a295J) (Typed v_a29lp) v_a295J v_a29lp
+ Swarm.Language.Typed: value :: forall v_a3EKy v_a3F1s. Lens (Typed v_a3EKy) (Typed v_a3F1s) v_a3EKy v_a3F1s
- Swarm.TUI.Border: bottomLabels :: forall n_a2k2A. Lens' (BorderLabels n_a2k2A) (HBorderLabels n_a2k2A)
+ Swarm.TUI.Border: bottomLabels :: forall n_a111R. Lens' (BorderLabels n_a111R) (HBorderLabels n_a111R)
- Swarm.TUI.Border: centerLabel :: forall n_a2k2B. Lens' (HBorderLabels n_a2k2B) (Maybe (Widget n_a2k2B))
+ Swarm.TUI.Border: centerLabel :: forall n_a111S. Lens' (HBorderLabels n_a111S) (Maybe (Widget n_a111S))
- Swarm.TUI.Border: leftLabel :: forall n_a2k2B. Lens' (HBorderLabels n_a2k2B) (Maybe (Widget n_a2k2B))
+ Swarm.TUI.Border: leftLabel :: forall n_a111S. Lens' (HBorderLabels n_a111S) (Maybe (Widget n_a111S))
- Swarm.TUI.Border: rightLabel :: forall n_a2k2B. Lens' (HBorderLabels n_a2k2B) (Maybe (Widget n_a2k2B))
+ Swarm.TUI.Border: rightLabel :: forall n_a111S. Lens' (HBorderLabels n_a111S) (Maybe (Widget n_a111S))
- Swarm.TUI.Border: topLabels :: forall n_a2k2A. Lens' (BorderLabels n_a2k2A) (HBorderLabels n_a2k2A)
+ Swarm.TUI.Border: topLabels :: forall n_a111R. Lens' (BorderLabels n_a111R) (HBorderLabels n_a111R)
- Swarm.TUI.Controller: keyToDir :: Key -> V2 Int64
+ Swarm.TUI.Controller: keyToDir :: Key -> Heading
- Swarm.TUI.Controller: scrollView :: (V2 Int64 -> V2 Int64) -> EventM Name AppState ()
+ Swarm.TUI.Controller: scrollView :: (Location -> Location) -> EventM Name AppState ()
- Swarm.TUI.Model: AppOpts :: Maybe Seed -> Maybe FilePath -> Maybe FilePath -> Bool -> Bool -> Maybe Port -> Maybe GitInfo -> AppOpts
+ Swarm.TUI.Model: AppOpts :: Maybe Seed -> Maybe FilePath -> Maybe FilePath -> Bool -> Bool -> Maybe ColorMode -> Maybe Port -> Maybe GitInfo -> AppOpts
- Swarm.TUI.Model: CancelButton :: ButtonSelection
+ Swarm.TUI.Model: CancelButton :: Button
- Swarm.TUI.Model: GoalModal :: [Text] -> ModalType
+ Swarm.TUI.Model: GoalModal :: ModalType
- Swarm.TUI.Model: InfoPanel :: Name
+ Swarm.TUI.Model: InfoPanel :: FocusablePanel
- Swarm.TUI.Model: KeepPlayingButton :: ButtonSelection
+ Swarm.TUI.Model: KeepPlayingButton :: Button
- Swarm.TUI.Model: Modal :: ModalType -> Dialog ButtonSelection -> Modal
+ Swarm.TUI.Model: Modal :: ModalType -> Dialog ButtonAction Name -> Modal
- Swarm.TUI.Model: NextButton :: ScenarioInfoPair -> ButtonSelection
+ Swarm.TUI.Model: NextButton :: Button
- Swarm.TUI.Model: QuitButton :: ButtonSelection
+ Swarm.TUI.Model: QuitButton :: Button
- Swarm.TUI.Model: REPLPanel :: Name
+ Swarm.TUI.Model: REPLPanel :: FocusablePanel
- Swarm.TUI.Model: RobotPanel :: Name
+ Swarm.TUI.Model: RobotPanel :: FocusablePanel
- Swarm.TUI.Model: StartOverButton :: Seed -> ScenarioInfoPair -> ButtonSelection
+ Swarm.TUI.Model: StartOverButton :: Button
- Swarm.TUI.Model: WorldPanel :: Name
+ Swarm.TUI.Model: WorldPanel :: FocusablePanel
- Swarm.TUI.Model: [_modalDialog] :: Modal -> Dialog ButtonSelection
+ Swarm.TUI.Model: [_modalDialog] :: Modal -> Dialog ButtonAction Name
- Swarm.TUI.Model: modalDialog :: Lens' Modal (Dialog ButtonSelection)
+ Swarm.TUI.Model: modalDialog :: Lens' Modal (Dialog ButtonAction Name)
- Swarm.Util: getElemsInArea :: V2 Int64 -> Int64 -> Map (V2 Int64) e -> [e]
+ Swarm.Util: getElemsInArea :: Location -> Int32 -> Map Location e -> [e]
- Swarm.Util: manhattan :: V2 Int64 -> V2 Int64 -> Int64
+ Swarm.Util: manhattan :: Location -> Location -> Int32
- Swarm.Web: mkApp :: IORef GameState -> Server SwarmApi
+ Swarm.Web: mkApp :: IORef AppState -> Server SwarmApi
- Swarm.Web: startWebThread :: Maybe Port -> IORef GameState -> IO (Either String Port)
+ Swarm.Web: startWebThread :: Maybe Port -> IORef AppState -> IO (Either String Port)
- Swarm.Web: type SwarmApi = "robots" :> Get '[JSON] [Robot] :<|> "robot" :> Capture "id" Int :> Get '[JSON] (Maybe Robot)
+ Swarm.Web: type SwarmApi = "robots" :> Get '[JSON] [Robot] :<|> "robot" :> Capture "id" Int :> Get '[JSON] (Maybe Robot) :<|> "goals" :> "prereqs" :> Get '[JSON] [PrereqSatisfaction] :<|> "goals" :> "active" :> Get '[JSON] [Objective] :<|> "goals" :> "graph" :> Get '[JSON] (Maybe GraphInfo) :<|> "goals" :> "uigoal" :> Get '[JSON] GoalTracking :<|> "goals" :> Get '[JSON] WinCondition :<|> "repl" :> "history" :> "full" :> Get '[JSON] [Text]
- Swarm.Web: webMain :: Maybe (MVar (Either String ())) -> Port -> IORef GameState -> IO ()
+ Swarm.Web: webMain :: Maybe (MVar (Either String ())) -> Port -> IORef AppState -> IO ()
Files
- CHANGELOG.md +78/−0
- app/Main.hs +26/−3
- bench/Benchmark.hs +6/−7
- data/entities.yaml +139/−30
- data/recipes.yaml +38/−0
- data/scenarios/Challenges/00-ORDER.txt +6/−0
- data/scenarios/Challenges/Mazes/easy_cave_maze.yaml +0/−1
- data/scenarios/Challenges/Mazes/easy_spiral_maze.yaml +0/−1
- data/scenarios/Challenges/Mazes/invisible_maze.yaml +0/−3
- data/scenarios/Challenges/Mazes/loopy_maze_sol.sw +0/−4
- data/scenarios/Challenges/Ranching/00-ORDER.txt +1/−0
- data/scenarios/Challenges/Ranching/_gated-paddock/enclosure-checking.sw +255/−0
- data/scenarios/Challenges/Ranching/_gated-paddock/fence-construction.sw +217/−0
- data/scenarios/Challenges/Ranching/_gated-paddock/meandering-sheep.sw +144/−0
- data/scenarios/Challenges/Ranching/gated-paddock.yaml +543/−0
- data/scenarios/Challenges/_bucket-brigade/brigade.sw +94/−0
- data/scenarios/Challenges/_bucket-brigade/hauler.sw +44/−0
- data/scenarios/Challenges/_bucket-brigade/powerplant.sw +20/−0
- data/scenarios/Challenges/_friend/cat.sw +75/−0
- data/scenarios/Challenges/_friend/friend-solution.sw +18/−0
- data/scenarios/Challenges/_ice-cream/customer.sw +60/−0
- data/scenarios/Challenges/_ice-cream/solution.sw +68/−0
- data/scenarios/Challenges/_wolf-goat-cabbage/cross.sw +40/−0
- data/scenarios/Challenges/_wolf-goat-cabbage/multi-item-possession.sw +4/−0
- data/scenarios/Challenges/_wolf-goat-cabbage/together-on-east-bank.sw +28/−0
- data/scenarios/Challenges/_wolf-goat-cabbage/unattended-together.sw +35/−0
- data/scenarios/Challenges/_word-search/create-puzzle.sw +247/−0
- data/scenarios/Challenges/_word-search/solution.sw +127/−0
- data/scenarios/Challenges/_word-search/verify-solution.sw +183/−0
- data/scenarios/Challenges/bucket-brigade.yaml +216/−0
- data/scenarios/Challenges/friend.yaml +94/−0
- data/scenarios/Challenges/hanoi.yaml +4/−4
- data/scenarios/Challenges/ice-cream.yaml +241/−0
- data/scenarios/Challenges/teleport.yaml +4/−3
- data/scenarios/Challenges/wolf-goat-cabbage.yaml +125/−0
- data/scenarios/Challenges/word-search.yaml +387/−0
- data/scenarios/Fun/00-ORDER.txt +1/−0
- data/scenarios/Fun/_logo-burst/coordinator.sw +12/−0
- data/scenarios/Fun/_logo-burst/drone.sw +67/−0
- data/scenarios/Fun/logo-burst.yaml +84/−0
- data/scenarios/Speedruns/curry.yaml +4/−2
- data/scenarios/Speedruns/forester.yaml +3/−1
- data/scenarios/Speedruns/mithril.yaml +3/−1
- data/scenarios/Testing/00-ORDER.txt +9/−0
- data/scenarios/Testing/1034-custom-attributes.yaml +134/−0
- data/scenarios/Testing/201-require/201-require-device-creative.yaml +2/−2
- data/scenarios/Testing/201-require/201-require-device-creative1.yaml +2/−2
- data/scenarios/Testing/201-require/201-require-device.yaml +2/−2
- data/scenarios/Testing/201-require/533-reprogram-simple.yaml +7/−7
- data/scenarios/Testing/201-require/533-reprogram.yaml +5/−5
- data/scenarios/Testing/378-objectives.yaml +4/−1
- data/scenarios/Testing/394-build-drill.yaml +8/−2
- data/scenarios/Testing/397-wrong-missing.yaml +38/−0
- data/scenarios/Testing/562-lodestone.sw +0/−2
- data/scenarios/Testing/562-lodestone.yaml +1/−0
- data/scenarios/Testing/795-prerequisite/00-ORDER.txt +4/−0
- data/scenarios/Testing/795-prerequisite/795-prerequisite-and.yaml +50/−0
- data/scenarios/Testing/795-prerequisite/795-prerequisite-cycle-with-not.yaml +42/−0
- data/scenarios/Testing/795-prerequisite/795-prerequisite-mutually-exclusive.yaml +61/−0
- data/scenarios/Testing/795-prerequisite/795-prerequisite-or.yaml +49/−0
- data/scenarios/Testing/858-inventory/00-ORDER.txt +3/−0
- data/scenarios/Testing/858-inventory/858-counting-objective.yaml +36/−0
- data/scenarios/Testing/858-inventory/858-nonpossession-objective.yaml +35/−0
- data/scenarios/Testing/858-inventory/858-possession-objective.yaml +33/−0
- data/scenarios/Testing/920-meet.yaml +52/−0
- data/scenarios/Testing/955-heading.yaml +28/−0
- data/scenarios/Testing/956-GPS.yaml +66/−0
- data/scenarios/Testing/958-isempty.yaml +57/−0
- data/scenarios/Testing/961-custom-capabilities.yaml +42/−0
- data/scenarios/Testing/_Validation/795-prerequisite-cycle.yaml +38/−0
- data/scenarios/Testing/_Validation/795-prerequisite-nonexistent-reference.yaml +38/−0
- data/scenarios/Testing/_Validation/795-prerequisite-self-reference.yaml +31/−0
- data/scenarios/Tutorials/00-ORDER.txt +2/−2
- data/scenarios/Tutorials/bind.yaml +0/−81
- data/scenarios/Tutorials/bind2-solution.sw +11/−0
- data/scenarios/Tutorials/bind2.yaml +111/−0
- data/scenarios/Tutorials/build.yaml +7/−20
- data/scenarios/Tutorials/conditionals.yaml +0/−3
- data/scenarios/Tutorials/craft.yaml +5/−0
- data/scenarios/Tutorials/crash-secret.sw +26/−28
- data/scenarios/Tutorials/def.yaml +0/−1
- data/scenarios/Tutorials/equip.yaml +61/−0
- data/scenarios/Tutorials/farming.sw +1/−7
- data/scenarios/Tutorials/farming.yaml +5/−1
- data/scenarios/Tutorials/install.yaml +0/−60
- data/scenarios/Tutorials/lambda.yaml +0/−1
- data/scenarios/Tutorials/move.yaml +12/−3
- data/scenarios/Tutorials/require.yaml +6/−7
- data/scenarios/Tutorials/requireinv.yaml +2/−3
- data/scenarios/Tutorials/world101.yaml +10/−3
- data/scenarios/classic.yaml +1/−0
- data/test/language-snippets/warnings/unused-vars/multiple-lambda-first-unused.sw +3/−0
- data/test/language-snippets/warnings/unused-vars/multiple-lambda-second-unused.sw +3/−0
- data/test/language-snippets/warnings/unused-vars/multiple-let-all-unused.sw +3/−0
- data/test/language-snippets/warnings/unused-vars/multiple-let-first-unused.sw +3/−0
- data/test/language-snippets/warnings/unused-vars/multiple-let-second-unused.sw +3/−0
- data/test/language-snippets/warnings/unused-vars/recursive-let.sw +1/−0
- data/test/language-snippets/warnings/unused-vars/shadowed-variable-lambda-unused.sw +1/−0
- data/test/language-snippets/warnings/unused-vars/shadowed-variable-let-intermediate-use.sw +4/−0
- data/test/language-snippets/warnings/unused-vars/shadowed-variable-let-unused.sw +3/−0
- data/test/language-snippets/warnings/unused-vars/single-bind-unused.sw +2/−0
- data/test/language-snippets/warnings/unused-vars/single-bind-used.sw +2/−0
- editors/emacs/swarm-mode.el +14/−7
- editors/vscode/syntaxes/swarm.tmLanguage.json +3/−3
- example/BFS-clear.sw +1/−1
- example/list.sw +2/−2
- src/Data/BoolExpr.hs +337/−0
- src/Data/BoolExpr/Simplify.hs +57/−0
- src/Swarm/App.hs +21/−16
- src/Swarm/DocGen.hs +22/−24
- src/Swarm/Game/CESK.hs +105/−96
- src/Swarm/Game/Display.hs +8/−8
- src/Swarm/Game/Entity.hs +62/−42
- src/Swarm/Game/Exception.hs +31/−31
- src/Swarm/Game/Log.hs +74/−0
- src/Swarm/Game/Recipe.hs +4/−6
- src/Swarm/Game/Robot.hs +58/−79
- src/Swarm/Game/Scenario.hs +22/−173
- src/Swarm/Game/Scenario/Cell.hs +62/−0
- src/Swarm/Game/Scenario/Objective.hs +195/−0
- src/Swarm/Game/Scenario/Objective/Graph.hs +144/−0
- src/Swarm/Game/Scenario/Objective/Logic.hs +46/−0
- src/Swarm/Game/Scenario/Objective/Presentation/Model.hs +117/−0
- src/Swarm/Game/Scenario/Objective/Presentation/Render.hs +91/−0
- src/Swarm/Game/Scenario/Objective/Validation.hs +50/−0
- src/Swarm/Game/Scenario/Objective/WinCheck.hs +83/−0
- src/Swarm/Game/Scenario/RobotLookup.hs +51/−0
- src/Swarm/Game/Scenario/Style.hs +37/−0
- src/Swarm/Game/Scenario/WorldDescription.hs +64/−0
- src/Swarm/Game/ScenarioInfo.hs +39/−23
- src/Swarm/Game/State.hs +91/−42
- src/Swarm/Game/Step.hs +465/−229
- src/Swarm/Game/Value.hs +0/−117
- src/Swarm/Game/World.hs +36/−13
- src/Swarm/Game/WorldGen.hs +6/−6
- src/Swarm/Language/Capability.hs +23/−10
- src/Swarm/Language/Context.hs +0/−1
- src/Swarm/Language/Elaborate.hs +39/−26
- src/Swarm/Language/LSP.hs +74/−27
- src/Swarm/Language/LSP/Hover.hs +260/−0
- src/Swarm/Language/LSP/VarUsage.hs +108/−0
- src/Swarm/Language/Module.hs +51/−0
- src/Swarm/Language/Parse.hs +55/−38
- src/Swarm/Language/Pipeline.hs +14/−11
- src/Swarm/Language/Pipeline/QQ.hs +2/−1
- src/Swarm/Language/Pretty.hs +7/−4
- src/Swarm/Language/Requirement.hs +6/−5
- src/Swarm/Language/Syntax.hs +420/−215
- src/Swarm/Language/Typecheck.hs +169/−126
- src/Swarm/Language/Types.hs +11/−42
- src/Swarm/Language/Value.hs +122/−0
- src/Swarm/TUI/Attr.hs +8/−2
- src/Swarm/TUI/Controller.hs +341/−153
- src/Swarm/TUI/Controller/Util.hs +53/−0
- src/Swarm/TUI/Model.hs +50/−729
- src/Swarm/TUI/Model/Achievement/Attainment.hs +47/−0
- src/Swarm/TUI/Model/Achievement/Definitions.hs +89/−0
- src/Swarm/TUI/Model/Achievement/Description.hs +70/−0
- src/Swarm/TUI/Model/Achievement/Persistence.hs +58/−0
- src/Swarm/TUI/Model/Failure.hs +51/−0
- src/Swarm/TUI/Model/Menu.hs +129/−0
- src/Swarm/TUI/Model/Name.hs +57/−0
- src/Swarm/TUI/Model/Repl.hs +266/−0
- src/Swarm/TUI/Model/StateUpdate.hs +158/−0
- src/Swarm/TUI/Model/UI.hs +312/−0
- src/Swarm/TUI/View.hs +200/−258
- src/Swarm/TUI/View/Achievement.hs +103/−0
- src/Swarm/TUI/View/CellDisplay.hs +59/−0
- src/Swarm/TUI/View/CustomStyling.hs +37/−0
- src/Swarm/TUI/View/Util.hs +159/−0
- src/Swarm/Util.hs +71/−43
- src/Swarm/Util/Location.hs +59/−0
- src/Swarm/Web.hs +58/−12
- swarm.cabal +66/−30
- test/integration/Main.hs +106/−45
- test/unit/Main.hs +4/−0
- test/unit/TestBoolExpr.hs +103/−0
- test/unit/TestEval.hs +24/−2
- test/unit/TestLSP.hs +88/−0
- test/unit/TestLanguagePipeline.hs +41/−2
- test/unit/TestNotification.hs +1/−1
- test/unit/TestPretty.hs +8/−3
- test/unit/TestUtil.hs +1/−1
CHANGELOG.md view
@@ -1,5 +1,83 @@ # Revision history for swarm +## **0.3.0.0** - 2023-01-30++This is the first release of 2023! It contains:+- various new features and bugfixes (see subsections below)+- UI tweaks+- fewer typos (thanks @bwignall and @kostmo)+- upgraded dependencies (`>=lsp-1.6` and `>=brick-1.5`) and raised Stack resolver to use GHC 9.4 [#1001](https://github.com/swarm-game/swarm/pull/1001)+- a ton of internal refactoring++### Fixed bugs+* copy parent robot context to child when executing `build` by @byorgey in [#817](https://github.com/swarm-game/swarm/pull/817)+* Merge new requirements context with existing when running from REPL by @byorgey in [#965](https://github.com/swarm-game/swarm/pull/965)+* Reset `lastFrameTime` when starting a new scenario by @byorgey in [#855](https://github.com/swarm-game/swarm/pull/855)+* Fix capability checking, and refactor/add lots of comments by @byorgey in [#959](https://github.com/swarm-game/swarm/pull/959)+* Fix `EntityMap` merging by @byorgey in [#962](https://github.com/swarm-game/swarm/pull/962)+* Fix: add first heard message to log by @xsebek in [#842](https://github.com/swarm-game/swarm/pull/842)+* Fix hypothetical result of condition by @xsebek in [#940](https://github.com/swarm-game/swarm/pull/940)+* One file per achievement record by @kostmo in [#954](https://github.com/swarm-game/swarm/pull/954)+* Show duplicate robot logs by @xsebek in [#1022](https://github.com/swarm-game/swarm/pull/1022)+* Copy requirements map to robot context when loading a new `ProcessedTerm` by @byorgey in [#827](https://github.com/swarm-game/swarm/pull/827)+* Set REPL to `Working` when base has `program` field specified by @byorgey in [#846](https://github.com/swarm-game/swarm/pull/846)+* Create swarm subdirectories as necessary by @xsebek in [#943](https://github.com/swarm-game/swarm/pull/943)++### New Features++#### Swarm language+* implement Equip/Unequip by @kostmo in [#887](https://github.com/swarm-game/swarm/pull/887)+ * The great `install`/`equip` switch by @byorgey in [#989](https://github.com/swarm-game/swarm/pull/989)+* Meeting other robots by @byorgey in [#920](https://github.com/swarm-game/swarm/pull/920)+* New `heading` command by @byorgey in [#955](https://github.com/swarm-game/swarm/pull/955)+* Add `isempty : cmd bool` to check whether current cell has an entity by @byorgey in [#968](https://github.com/swarm-game/swarm/pull/968)++#### Entities+* Add `GPS receiver` device to provide `senseloc` capability by @byorgey in [#956](https://github.com/swarm-game/swarm/pull/956)+* Device with recipe for 'appear' capability by @kostmo in [#1027](https://github.com/swarm-game/swarm/pull/1027)+* device for setname by @kostmo in [#1028](https://github.com/swarm-game/swarm/pull/1028)++#### LSP improvements+* swarm-lang IDE hover by @kostmo in [#972](https://github.com/swarm-game/swarm/pull/972)+* Include `SrcLoc` info with variable binding sites by @byorgey in [#993](https://github.com/swarm-game/swarm/pull/993)+* Annotate ASTs with types at every node by @byorgey in [#991](https://github.com/swarm-game/swarm/pull/991)+* Improve OnHover for Def/Let, App and Const by @xsebek in [#986](https://github.com/swarm-game/swarm/pull/986)+* warn of unused variables by @kostmo in [#983](https://github.com/swarm-game/swarm/pull/983)++#### Web API+* Expose REPL history via web interface by @kostmo in [#821](https://github.com/swarm-game/swarm/pull/821)++#### Command line options+* Add full color mode option by @xsebek in [#851](https://github.com/swarm-game/swarm/pull/851)++#### New scenarios+* New ranching scenario by @kostmo in [#835](https://github.com/swarm-game/swarm/pull/835)+* new bind tutorial by @kostmo in [#895](https://github.com/swarm-game/swarm/pull/895)+* A "bucket brigade" of robots by @kostmo in [#885](https://github.com/swarm-game/swarm/pull/885)+* Ice Cream Shop by @kostmo in [#1008](https://github.com/swarm-game/swarm/pull/1008)+* word search by @kostmo in [#999](https://github.com/swarm-game/swarm/pull/999)+* Wolf, Goat, Cabbage scenario by @kostmo in [#944](https://github.com/swarm-game/swarm/pull/944)+* swarm logo animation by @kostmo in [#1018](https://github.com/swarm-game/swarm/pull/1018)++#### Scenario development+* Boolean expressions of objective prerequisites by @kostmo in [#927](https://github.com/swarm-game/swarm/pull/927)+* support 'orientationMap' by @kostmo in [#1023](https://github.com/swarm-game/swarm/pull/1023)+* support custom attributes defined in scenarios by @kostmo in [#1058](https://github.com/swarm-game/swarm/pull/1058)++#### UI enhancements+* show devices that enable commands by @kostmo in [#899](https://github.com/swarm-game/swarm/pull/899)+* Add a "driving mode" to the REPL by @kostmo in [#819](https://github.com/swarm-game/swarm/pull/819)++#### Achievements+* achievements board by @kostmo in [#796](https://github.com/swarm-game/swarm/pull/796)+* first tutorial completion achievement by @kostmo in [#925](https://github.com/swarm-game/swarm/pull/925)+* Add achievement for losing by @kostmo in [#945](https://github.com/swarm-game/swarm/pull/945)++#### Debugging+* Better CESK machine pretty-printing by @byorgey in [#948](https://github.com/swarm-game/swarm/pull/948)+* Add the full expected location for the data directory by @TristanCacqueray in [#908](https://github.com/swarm-game/swarm/pull/908)+* Extend logs for warnings by @xsebek in [#982](https://github.com/swarm-game/swarm/pull/982)+ ## **0.2.0.0** - 2022-11-01 A bunch of small fixes and improvements; special thanks to new
app/Main.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TemplateHaskell #-} @@ -14,11 +15,12 @@ import Swarm.DocGen (EditorType (..), GenerateDocs (..), PageAddress (..), SheetType (..), generateDocs) import Swarm.Language.LSP (lspMain) import Swarm.Language.Pipeline (processTerm)-import Swarm.TUI.Model (AppOpts (..))+import Swarm.TUI.Model (AppOpts (..), ColorMode (..)) import Swarm.Version import Swarm.Web (defaultPort) import System.Exit (exitFailure, exitSuccess) import System.IO (hPrint, stderr)+import Text.Read (readMaybe) gitInfo :: Maybe GitInfo gitInfo = either (const Nothing) Just ($$tGitInfoCwdTry)@@ -45,7 +47,17 @@ , command "version" (info (pure Version) (progDesc "Get current and upstream version.")) ] )- <|> Run <$> (AppOpts <$> seed <*> scenario <*> run <*> autoplay <*> cheat <*> webPort <*> pure gitInfo)+ <|> Run+ <$> ( AppOpts+ <$> seed+ <*> scenario+ <*> run+ <*> autoplay+ <*> cheat+ <*> color+ <*> webPort+ <*> pure gitInfo+ ) where format :: Parser CLI format =@@ -98,13 +110,24 @@ <> help ("Set the web service port (or disable it with 0). Default to " <> show defaultPort <> ".") ) scenario :: Parser (Maybe String)- scenario = optional $ strOption (long "scenario" <> short 'c' <> metavar "FILE" <> help "Name of a scenario to load")+ scenario = optional $ strOption (long "scenario" <> short 'i' <> metavar "FILE" <> help "Name of an input scenario to load") run :: Parser (Maybe String) run = optional $ strOption (long "run" <> short 'r' <> metavar "FILE" <> help "Run the commands in a file at startup") autoplay :: Parser Bool autoplay = switch (long "autoplay" <> short 'a' <> help "Automatically run the solution defined in the scenario, if there is one. Mutually exclusive with --run.") cheat :: Parser Bool cheat = switch (long "cheat" <> short 'x' <> help "Enable cheat mode. This allows toggling Creative Mode with Ctrl+v and unlocks \"Testing\" scenarios in the menu.")+ color :: Parser (Maybe ColorMode)+ color = optional $ option colorModeParser (long "color" <> short 'c' <> metavar "MODE" <> help "Use none/8/16/full color mode.")+ colorModeParser =+ Data.Foldable.asum+ [ ColorMode8 <$ text "8"+ , ColorMode16 <$ text "16"+ , ColorMode240 <$> maybeReader (\case ('2' : '4' : '0' : '_' : w) -> readMaybe w; _ -> Nothing)+ , FullColor <$ text "full"+ , NoColor <$ text "none"+ ]+ text t = maybeReader (\x -> if x == t then Just x else Nothing) cliInfo :: ParserInfo CLI cliInfo =
bench/Benchmark.hs view
@@ -9,8 +9,6 @@ import Control.Monad.State (evalStateT, execStateT) import Criterion.Main (Benchmark, bench, bgroup, defaultConfig, defaultMainWith, whnfAppIO) import Criterion.Types (Config (timeLimit))-import Data.Int (Int64)-import Linear.V2 (V2 (V2)) import Swarm.Game.CESK (emptyStore, initMachine) import Swarm.Game.Display (defaultRobotDisplay) import Swarm.Game.Robot (TRobot, mkRobot)@@ -22,13 +20,14 @@ import Swarm.Language.Pipeline (ProcessedTerm) import Swarm.Language.Pipeline.QQ (tmQ) import Swarm.Language.Syntax (north)+import Swarm.Util.Location -- | The program of a robot that does nothing. idleProgram :: ProcessedTerm idleProgram = [tmQ| {} |] -- | The program of a robot which waits a random number of ticks, changes its--- appearence, then waits another random number of ticks, places a tree, and+-- appearance, then waits another random number of ticks, places a tree, and -- then self-destructs. treeProgram :: ProcessedTerm treeProgram =@@ -70,14 +69,14 @@ |] -- | Initializes a robot with program prog at location loc facing north.-initRobot :: ProcessedTerm -> V2 Int64 -> TRobot-initRobot prog loc = mkRobot () Nothing "" [] (Just north) loc defaultRobotDisplay (initMachine prog Context.empty emptyStore) [] [] False False 0+initRobot :: ProcessedTerm -> Location -> TRobot+initRobot prog loc = mkRobot () Nothing "" [] (Just loc) north defaultRobotDisplay (initMachine prog Context.empty emptyStore) [] [] False False 0 -- | Creates a GameState with numRobot copies of robot on a blank map, aligned -- in a row starting at (0,0) and spreading east.-mkGameState :: (V2 Int64 -> TRobot) -> Int -> IO GameState+mkGameState :: (Location -> TRobot) -> Int -> IO GameState mkGameState robotMaker numRobots = do- let robots = [robotMaker (V2 (fromIntegral x) 0) | x <- [0 .. numRobots - 1]]+ let robots = [robotMaker (Location (fromIntegral x) 0) | x <- [0 .. numRobots - 1]] Right initState <- runExceptT classicGame0 execStateT (mapM addTRobot robots)
data/entities.yaml view
@@ -74,6 +74,16 @@ Can be used to build a simple furnace, among other things. properties: [portable] +- name: handle+ display:+ attr: device+ char: 'h'+ description:+ - Ergonomic affordance for manipulating objects. Also grants the "setname" capability, allowing one+ to assign a "handle" to robots.+ properties: [portable]+ capabilities: [setname]+ - name: lodestone display: attr: iron@@ -84,6 +94,15 @@ properties: [portable] capabilities: [negation] +- name: beaglepuss+ display:+ attr: rubber+ char: 'B'+ description:+ - Iconic novelty disguise. Renders one either completely inconspicuous, or the opposite.+ properties: [portable]+ capabilities: [appear]+ - name: boulder display: attr: rock@@ -106,7 +125,7 @@ char: 'Å' priority: 11 description:- - A tunnel in a mountain through which robots can freely move.+ - A tunnel in a mountain through which anything can freely move. properties: [] - name: copper ore@@ -326,15 +345,43 @@ information, made of twisted cotton fibers. Multiple strings can also be woven into larger configurations such as cloth or nets. - |- A string device enables two commands: the first, `format : a ->- text`, can turn any value into a suitable text representation.- The second is the infix operator `++ : text -> text -> text`- which can be used to concatenate two text values. For example,+ An equipped `string` device enables several commands for working with+ `text` values: - |+ `format : a -> text` can turn any value into a suitable text+ representation.+ - |+ The infix operator `++ : text -> text -> text`+ can be used to concatenate two text values. For example,+ - | "Number of widgets: " ++ format numWidgets+ - |+ `chars : text -> int` computes the number of characters in a+ `text` value.+ - |+ `split : int -> text -> text * text` splits a `text` value into+ two pieces, one before the given index and one after. properties: [portable] capabilities: [text] +- name: decoder ring+ display:+ attr: silver+ char: 'O'+ description:+ - |+ A handy lookup table for converting from characters to numeric codes and+ back, shaped for some reason into a ring. When equipped, it+ enables two functions:+ - |+ `charAt : int -> text -> int` returns the numeric code of the+ character at a specific index in a (0-indexed) `text` value.+ - |+ `toChar : int -> text` creates a singleton (length-1) `text`+ value containing a character with the given numeric code.+ properties: [portable]+ capabilities: [code]+ - name: lambda display: attr: flower@@ -379,11 +426,11 @@ attr: wood char: 'B' description:- - A robot with a boat installed can float on top of water without drowning.+ - A robot with a boat equipped can float on top of water without drowning. - |- Note: most devices are automatically installed on robots that+ Note: most devices are automatically equipped on robots that will require them; but this doesn't work in the case of boats since floating is not- associated with any particular command. To manually ensure a boat is installed on+ associated with any particular command. To manually ensure a boat is equipped on a robot, just add the special command `require "boat"` to the robot's program. properties: [portable] capabilities: [float]@@ -645,7 +692,7 @@ attr: device char: '%' description:- - Installing treads on a robot allows it to move and turn.+ - Equipping treads on a robot allows it to move and turn. - The `move` command moves the robot forward one unit. - 'Example:' - ' move; move; // move two units'@@ -673,8 +720,8 @@ char: '<' description: - A grabber arm is an all-purpose, hydraulically controlled device that can- manipulate other items and robots via the `grab`, `place`, `give`,- and `install` commands.+ manipulate other items and robots via the `grab`, `place`, and `give`+ commands. - The `grab` command takes no arguments; it simply grabs whatever is available, and also returns the name of the grabbed thing as a string. It raises an exception if run in a cell that does not contain an item.@@ -682,13 +729,10 @@ place. The item is removed from the robot's inventory and placed in the robot's current cell (which must be empty). Raises an exception if the operation fails.- - "The `give` command takes two arguments: the robot to+ - "The `give` command takes two arguments: the actor to give an item to (which can be at most 1 cell away), and the name of the item to give. Raises an exception if the operation fails."- - "The `install` command takes two arguments: the robot- on which to install a device (which can be at most 1 cell away),- and the name of the device to install."- capabilities: [grab, give, place, install]+ capabilities: [grab, give, place] properties: [portable] - name: fast grabber@@ -697,7 +741,7 @@ char: '≪' description: - A fast grabber is an improved version of the basic grabber - not only- can it 'grab', 'place', 'give', and 'install', it can also 'swap'.+ can it 'grab', 'place', and 'give', it can also 'swap'. - The 'swap' command allows the robot to execute grab and place at the same time so that the location where the robot is standing does not become empty.@@ -706,9 +750,26 @@ - In addition you retain the capability to use the 'atomic' command, with which you can implement other commands that are safe when run in parallel.- capabilities: [grab, swap, give, place, install, atomic]+ capabilities: [grab, swap, give, place, atomic] properties: [portable] +- name: welder+ display:+ attr: device+ char: 'f'+ description:+ - |+ A welder can be used to affix other gadgets to one's robot, providing+ the `equip` and `unequip` commands.+ - |+ The `equip` command takes one argument: the name of the device to equip.+ Raises an exception if the device is not in your inventory.+ - |+ The `unequip` command takes one argument: the name of the device to unequip.+ Raises an exception if the device is not equipped.+ capabilities: [equip, unequip]+ properties: [portable]+ - name: harvester display: attr: device@@ -869,9 +930,13 @@ about their surroundings. Simply give `scan` a direction in which to scan, and information about the scanned item (if any) will be added to the robot's inventory."- - "A scanner also enables the `blocked` command, which returns a+ - "A scanner also enables `blocked : cmd bool`, which returns a boolean value indicating whether the robot's path is blocked- (i.e. whether executing a `move` command would fail)."+ (i.e. whether executing a `move` command would fail);+ `ishere : text -> cmd bool` for checking whether the current+ cell contains a particular entity; and `isempty : cmd bool` for+ checking whether the current cell is empty of entities. Note that+ `ishere` and `isempty` do not detect robots, only entities." - "Finally, robots can use the `upload` command to copy their accumulated knowledge to another nearby robot; for example, `upload base`." properties: [portable]@@ -921,10 +986,10 @@ attr: device char: '@' description:- - "Allows a robot to hear what nearby robots are saying."- - "Simply having this device installed will automatically- add messages said by nearby robots to this robot's log,- assuming it has a logger installed."+ - "Allows a robot to hear anything being said nearby."+ - "Simply having this device equipped will automatically+ add messages said by nearby actors to this robot's log,+ assuming it has a logger equipped." - "That way you can view any heard message later either in the logger or the message window." - "To wait for a message and get the string value, use:"@@ -996,9 +1061,16 @@ attr: device char: 'N' description:- - "A compass gives a robot the ability to orient using cardinal directions: north, south, west, and east."- - "Example:"- - "turn west; move; turn north"+ - |+ A compass gives a robot the ability to orient using the cardinal+ directions north, south, west, and east; for example, `turn west;+ move; turn north`.+ - |+ It also enables the `heading : cmd dir` command, which returns the+ robot's current heading. For example, the following code moves+ east and then restores the same heading as before:+ - |+ d <- heading; turn east; move; turn d properties: [portable] capabilities: [orient] @@ -1031,7 +1103,7 @@ attr: device char: 'Ю' description:- - An I/O cable can be used to communicate with an adjacent robot.+ - An I/O cable can be used to communicate with adjacent actors. properties: [portable] - name: rubber band@@ -1048,7 +1120,7 @@ - "This seems like a safe way to execute `grab` only when there is a rock to grab. However, it is actually possible for the `grab` to fail, if some other robot B snatches the rock right after robot A sensed- it and before robot A got around to grab it on the next game tick."+ it and before robot A got around to grabbing it on the next game tick." - "This will make robot A very sad and it will crash." - "To prevent this situation, robot A can wrap the commands in `atomic`, like so:" - |@@ -1063,9 +1135,46 @@ char: '#' description: - A net is a device woven out of many strings. With a net- installed, you can use the `try` command to catch errors. For example,+ equipped, you can use the `try` command to catch errors. For example, - | `try {move} {turn left}` - will attempt to move, but if that fails, turn left instead. properties: [portable] capabilities: [try]++- name: antenna+ display:+ attr: silver+ char: 'Y'+ description:+ - A device for transforming electric current into radio waves, and+ vice versa.+ - |+ It can be used to locate nearby actors, by bouncing radio+ waves off them and listening for the echo. This capability can be+ accessed via two commands:+ - |+ `meet : cmd (() + actor)` tries to locate a+ nearby actor (a robot, or... something else?) up to one cell away.+ It returns a reference to the nearest actor, or a unit value if+ none are found.+ - |+ `meetAll : (b -> actor -> cmd b) -> b -> cmd b` runs a command on+ every nearby actor (other than oneself), folding over the results+ to compute a final result of type `b`. For example, if `x`, `y`,+ and `z` are nearby actors, then `meetAll f b0` is equivalent to+ `b1 <- f b0 x; b2 <- f b1 y; f b2 z`.+ properties: [portable]+ capabilities: [meet]++- name: GPS receiver+ display:+ attr: device+ char: 'G'+ description:+ - |+ A GPS receiver triangulates your current (x,y) coordinates from+ some convenient satellite signals,+ enabling the command `whereami : cmd (int * int)`.+ properties: [portable]+ capabilities: [senseloc]
data/recipes.yaml view
@@ -459,6 +459,8 @@ - [4, big motor] - [64, iron plate] - [16, rubber]+ required:+ - [1, welder] out: - [1, tank treads] @@ -478,6 +480,21 @@ - [1, fast grabber] - in:+ - [2, copper wire]+ - [1, copper pipe]+ - [1, iron plate]+ - [1, I/O cable]+ out:+ - [1, welder]++- in:+ - [1, copper pipe]+ out:+ - [1, handle]+ required:+ - [1, welder]++- in: - [4, circuit] - [1, iron plate] - [3, copper pipe]@@ -508,6 +525,20 @@ out: - [1, bitcoin] +- in:+ - [2, silver]+ - [2, copper wire]+ out:+ - [1, antenna]++- in:+ - [1, antenna]+ - [1, clock]+ - [1, circuit]+ - [1, compass]+ out:+ - [1, GPS receiver]+ ## MAGIC - in:@@ -703,6 +734,13 @@ - [1, strange loop] out: - [1, rubber band]++- in:+ - [1, rubber]+ - [1, glass]+ - [1, string]+ out:+ - [1, beaglepuss] ######################################### ## COTTON ##
data/scenarios/Challenges/00-ORDER.txt view
@@ -1,5 +1,11 @@ chess_horse.yaml teleport.yaml 2048.yaml+word-search.yaml+ice-cream.yaml hanoi.yaml+bucket-brigade.yaml+wolf-goat-cabbage.yaml+friend.yaml Mazes+Ranching
data/scenarios/Challenges/Mazes/easy_cave_maze.yaml view
@@ -15,7 +15,6 @@ def until = \p. \c. b <- p; if b {} {c; until p c} end; def fwd = until blocked move end; build {- require "branch predictor"; // #540 turn east; until (ishere "goal") (fwd; turn right; fwd; turn left); grab
data/scenarios/Challenges/Mazes/easy_spiral_maze.yaml view
@@ -16,7 +16,6 @@ def until = \p. \c. b <- p; if b {} {c; until p c} end; def fwd = until blocked move end; build {- require "branch predictor"; // #540 turn west; until (ishere "goal") (fwd; turn right); grab
data/scenarios/Challenges/Mazes/invisible_maze.yaml view
@@ -25,9 +25,6 @@ def rightBlocked = tR; b <- blocked; tL; return b end; def mazeStep = ifC rightBlocked {ifC blocked {tL} {move}} {tR; move} end; build {- require "treads"; require "branch predictor"; require "lambda";- require "strange loop"; require "scanner"; require "grabber"; // #540- forever (mazeStep; ifC (ishere "goal") {grab; return ()} {}) } robots:
data/scenarios/Challenges/Mazes/loopy_maze_sol.sw view
@@ -14,9 +14,5 @@ tB; move; tB end; build {- require "treads"; require "scanner"; require "lambda";- require "strange loop"; require "branch predictor";- require "grabber"; // #540- require 500 "rock"; DFS }
+ data/scenarios/Challenges/Ranching/00-ORDER.txt view
@@ -0,0 +1,1 @@+gated-paddock.yaml
+ data/scenarios/Challenges/Ranching/_gated-paddock/enclosure-checking.sw view
@@ -0,0 +1,255 @@+// Algorithm:+// ----------+// Maintain current direction until a wall is encountered.+// Then enter "wall-following mode".+// This mode presumes the wall is not a loop.+// Wall-following mode exploits recursion to keep track of how many left turns were made+// and then unwinds them again by ensuring each is paired with a right turn.+// Once the recursion is fully unwound, the robot proceeds along its original direction+// (though it may now be laterally displaced).+//+// (If it was a loop, then an "oriented breadcrumb" would need to be left.+// The breadcrumb is oriented in case a single-width passage is backtracked+// along the opposite wall.)++/** A "gate" is walkable, so we need to supplement the "blocked" check with this function.+Since fences are "unwalkable", they do not need to be mentioned in this function.+*/+def isFenced =+ s <- scan forward;+ return (+ case s+ (\_. false)+ (\x. x == "gate")+ );+ end;++def isBlockedOrFenced =+ b <- blocked;+ f <- isFenced;+ return (b || f);+ end;++// Returns true if we've already placed two+// breadcrumbs on a given tile, false otherwise.+def leaveBreadcrumbs =+ + let bc1 = "fresh breadcrumb" in+ let bc2 = "treaded breadcrumb" in++ wasTraversedOnce <- ishere bc1;+ if wasTraversedOnce {+ _crumb <- grab;+ make bc2;+ place bc2;+ return false;+ } {+ wasTraversedTwice <- ishere bc2;+ if wasTraversedTwice {+ return true;+ } {+ // Make sure nothing's in the way before we place+ // our breadcrumb:+ x <- scan down;+ case x return (\y.+ // If we're on a water tile, get rid of+ // it with our special "drilling" recipe+ if (y == "water") {+ drill down;+ // Nothing will remain on the ground.+ // after making the "steam" via+ // the drilling recipe.+ } {+ grab;+ return ();+ };+ );++ make bc1;+ place bc1;+ return false;+ };+ };+ end;++def goForwardToPatrol = \wasBlocked.+ b <- isBlockedOrFenced;+ if b {+ turn left;+ goForwardToPatrol true;+ turn right;+ goForwardToPatrol false;+ } {+ if wasBlocked {+ isLoop <- leaveBreadcrumbs;+ if isLoop {+ fail "loop";+ } {};+ } {};+ move;+ };+ end;++/**+There should only be one place in the+code where an exception is thrown: that is,+if a treaded breadcrumb is encountered.+*/+def checkIsEnclosedInner =+ try {+ goForwardToPatrol false;+ // Water is the outer boundary+ hasWater <- ishere "water";+ if hasWater {+ return false;+ } {+ checkIsEnclosedInner;+ };+ } {+ return true;+ };+ end;++def checkIsEnclosed =++ // The "evaporator" drill is used+ // to clear water tiles.+ let specialDrill = "evaporator" in+ create specialDrill;+ equip specialDrill;++ // NOTE: System robots can walk on water+ // so we only need this if we want to+ // demo the algorithm with a player robot.+// create "boat";+// equip "boat";++ checkIsEnclosedInner;+ end;++def boolToInt = \b. if (b) {return 1} {return 0}; end;++def countAdjacentBlockages =++ turn left;+ b1 <- isBlockedOrFenced;+ c1 <- boolToInt b1;++ turn left;+ b2 <- isBlockedOrFenced;+ c2 <- boolToInt b2;++ turn left;+ b3 <- isBlockedOrFenced;+ c3 <- boolToInt b3;++ turn left;+ b4 <- isBlockedOrFenced;+ c4 <- boolToInt b4;++ return $ c1 + c2 + c3 + c4;+ end;++// Step forward, observing left and right.+def observeLeftAndRight =+ move;+ turn left;+ amBlockedLeft <- isBlockedOrFenced;+ val1 <- boolToInt amBlockedLeft;++ turn back;+ amBlockedRight <- isBlockedOrFenced;+ val2 <- boolToInt amBlockedRight;++ turn right;+ move;+ return $ val1 + val2;+ end;+++/** If the four cardinal directions have at most+one blockage, then there will exist an orientation+where both that direction and its opposite direction+are clear.+So we can step that direction, check to the left and+right of us, then step in the opposite direction+and do the same. This allows us to check the 4+blocks that touch the corners of the center block.+*/+def countDiagonalBlockages =+ // First, orient to the clear front-to-back path+ amBlocked <- isBlockedOrFenced;+ if amBlocked {turn left;} {};++ // Second, step to both sides+ fwdCount <- observeLeftAndRight;+ backCount <- observeLeftAndRight;+ return $ fwdCount + backCount;+ end;++def isStandingOnBridge =+ onFence <- ishere "fence";+ onGate <- ishere "gate";+ if (onFence || onGate) {+ adjCount <- countAdjacentBlockages;+ if (adjCount > 1) {+ return true;+ } {+ diagCount <- countDiagonalBlockages;+ return $ (adjCount + diagCount) > 1;+ };+ } {return false};+ end;++def getValForSheepIndex = \predicateCmd. \i.+ try {+ // This will throw an exception if+ // the sheep has already drowned.+ r <- robotnumbered i;+ didSucceed <- as r {predicateCmd};++ boolToInt didSucceed;+ } {+ return 0;+ }+ end;++/**+There are 3 sheep.+They have indices 1, 2, 3.+(The base has index 0).++THIS DOES NOT WORK!+*/+def countSheepWithRecursive = \predicateCmd. \i.++ if (i > 0) {+ val <- getValForSheepIndex predicateCmd i;+ recursiveCount <- countSheepWithRecursive predicateCmd $ i - 1;+ return $ val + recursiveCount;+ } {+ return 0;+ }+ end;+++def countSheepWith = \predicateCmd.++ val1 <- getValForSheepIndex predicateCmd 1;+ val2 <- getValForSheepIndex predicateCmd 2;+ val3 <- getValForSheepIndex predicateCmd 3;+ return $ val1 + val2 + val3;++ end;+++justFilledGap <- as base {+ isStandingOnBridge;+};++if (justFilledGap) {+ enclosedCount <- countSheepWith checkIsEnclosed;+ return $ enclosedCount >= 1;+} {+ return false;+}
+ data/scenarios/Challenges/Ranching/_gated-paddock/fence-construction.sw view
@@ -0,0 +1,217 @@+def doN = \n. \f. if (n > 0) {f; doN (n - 1) f} {}; end;++def processTree =+ grab;+ fenceCount <- count "fence";+ if (fenceCount < 90) {+ make "log";+ make "board";+ doN 2 $ make "fence";+ } {};+ end;++def makeHarvester =+ doN 3 $ make "log";+ doN 3 $ make "board";+ make "box";+ doN 2 $ make "wooden gear";+ make "harvester";+ equip "harvester";+ end;++def grabTwoRows =+ doN 7 (processTree; move);+ processTree;+ turn right;+ move;+ turn right;++ doN 7 (processTree; move);+ processTree;+ end;+++def grabTrees =+ doN 7 move;+ turn right;++ grabTwoRows;++ turn left;+ move;+ turn left;++ grabTwoRows;++ turn left;+ move;+ turn left;++ grabTwoRows;++ end;+++def harvestIfClover =+ x <- scan down;+ case x return (\y.+ if (y == "clover") {+ harvest;+ return ();+ } {};+ );+ end;+++def buildFence =+ doN 6 move;+ turn right;+ doN 4 (place "fence"; move);+ turn left;+ doN 30 (place "fence"; move);+ turn left;+ doN 15 (place "fence"; move);+ turn left;+ doN 30 (place "fence"; move);+ turn left;+ doN 10 (place "fence"; move);+ make "gate";+ place "gate";+ turn right;+ end;++def gatherClover =+ doN 8 move;+ turn left;+ doN 8 move;+ turn left;++ doN 19 (harvestIfClover; move;);+ turn right;+ move;+ turn right;+ doN 19 (harvestIfClover; move;);+ turn right;+ end;++def plantCloverColumn = \direction. \extraStep.+ doN 6 (+ extraStep;+ harvest;+ move;+ );++ turn direction;+ move;+ turn direction;+ move;+ end;++def plantCloverField =+ doN 4 move;+ + plantCloverColumn right $place "clover";+ plantCloverColumn left $ place "clover";+ plantCloverColumn right $ place "clover";+ plantCloverColumn left $ place "clover";+ plantCloverColumn right $ place "clover";+ end;+++def harvestCloverField =+ doN 5 move;+ turn right;+ doN 5 move;+ turn right;+ wait 200;+ + plantCloverColumn right $ return ();+ plantCloverColumn left $ return ();+ plantCloverColumn right $ return ();+ plantCloverColumn left $ return ();+ plantCloverColumn right $ return ();+ end;+++def travelRow = \action.+ doN 28 (action; move);+ action;+ end;++def placeCloverRow =+ travelRow $ place "clover";+ end;+++def distributeCloverInPaddock =+ turn left;+ doN 4 move;+ placeCloverRow;+ turn left;+ doN 2 move;+ turn left;+ placeCloverRow;+ turn right;+ doN 2 move;+ turn right;+ placeCloverRow;+ turn left;+ doN 2 move;+ turn left;+ placeCloverRow;+ end;+++def pickupWool =+ x <- scan down;+ case x return (\y.+ if (y == "wool") {+ grab;+ return ();+ } {};+ );+ end;++def sweep2rows =+ travelRow pickupWool;+ turn right;+ move;+ turn right;+ travelRow pickupWool;+ turn left;+ move;+ turn left;+ end;+++def sweepAreaForWool =+ doN 5 sweep2rows;+ turn left;+ doN 10 move;+ turn right;++ woolCount <- count "wool";+ if (woolCount >= 3) {+ make "sweater";+ } {};++ end;++def collectWool =+ wait 500;++ turn back;++ let forever : cmd unit -> cmd unit = \c. c ; forever c in+ forever sweepAreaForWool;+ end;+++grabTrees;+makeHarvester;+buildFence;+gatherClover;+plantCloverField;+doN 3 harvestCloverField;+distributeCloverInPaddock;+collectWool;
+ data/scenarios/Challenges/Ranching/_gated-paddock/meandering-sheep.sw view
@@ -0,0 +1,144 @@+// A "sheep" that wanders around randomly.++/** A "gate" is walkable, so we need to supplement the "blocked" check with this function.+Since fences are "unwalkable", they do not need to be mentioned in this function.+*/+def isFenced =+ s <- scan forward;+ return (+ case s+ (\_. false)+ (\x. x == "gate")+ );+ end;++def isBlockedOrFenced =+ b <- blocked;+ f <- isFenced;+ return (b || f);+ end;++def elif = \p.\t.\e. {if p t e} end;+++def turnToClover = \direction.++ x <- scan direction;+ case x (\_. return false;) (\y.+ if (y == "clover") {+ turn direction;+ return true;+ } {+ return false;+ };+ );+ end;+++/**+If there is adjacent clover,+turn that direction.+*/+def turnCloverDirection =++ foundN <- turnToClover north;+ if (foundN) {return true} {+ foundE <- turnToClover east; + if (foundE) {return true} {+ foundS <- turnToClover south;+ if (foundS) {return true} {+ turnToClover west;+ }+ }+ }+ end;++def decideDirection =++ let randdir : cmd dir =+ d <- random 4;+ return $ if (d == 0) {+ north+ } $ elif (d == 1) {+ east+ } $ elif (d == 2) {+ south+ } {+ west+ }+ in++ cloverCount <- count "clover";+ if (cloverCount > 4) {+ d <- randdir;+ turn d;+ } {+ nearClover <- turnCloverDirection;+ if (nearClover) {} {+ d <- randdir;+ turn d;+ }+ }+ end;++let forever : cmd unit -> cmd unit = \c. c ; forever c in+let repeat : int -> cmd unit -> cmd unit =+ \n. \c. if (n == 0) {} {c ; repeat (n-1) c} in+++forever (+ n <- random 30;+ wait (30 + n);++ decideDirection;++ dist <- random 3;+ repeat dist (++ b <- isBlockedOrFenced;+ if b {} {+ move;+ };++ // Sheep can drown.+ hasWater <- ishere "water";+ if hasWater {+ say "The jolly jumbuck sprang into the billabong. \"You'll never catch me alive!\", said he.";+ selfdestruct;+ } {};++ // Eat clover.+ x <- scan down;+ case x return (\y.+ if (y == "clover") {+ harvest;+ cloverCount <- count "clover";+ if (cloverCount < 2) {+ say "yum!"+ } {};+ } {};+ );+ );+ r <- random 30;+ if (r == 0) { say "baaa" } {};++ hasClover <- has "clover";+ if (hasClover) {+ r <- random 8;+ if (r == 0) { + let item = "wool" in+ hasWool <- has item;+ if (hasWool) {+ // Make sure nothing's in the way before we place+ // our wool:+ x <- scan down;+ case x return (\_.+ grab;+ return ();+ );++ place item;+ } {};+ } {};+ } {};+)
+ data/scenarios/Challenges/Ranching/gated-paddock.yaml view
@@ -0,0 +1,543 @@+version: 1+name: Pastoral Island+author: Karl Ostmo+description: |+ Start a wool industry from the local fauna.+creative: false+objectives:+ - id: enclose_sheep+ teaser: Paddock sheep+ goal:+ - |+ You've homesteaded on a small island in the ocean.+ It's time to gather resources to trade.+ - |+ You encounter some feral sheep (@). They slowly wander the island and eat grass.+ Your mind wanders to textiles...+ - |+ First, paddock at least one sheep so they don't drown.+ Make sure there are no gaps in the fence!+ - |+ Note that you can use the "drill" command (by way of the "post puller" tool)+ to demolish a fence that has been "placed".+ condition: |-+ // Algorithm:+ // ----------+ // Maintain current direction until a wall is encountered.+ // Then enter "wall-following mode".+ // This mode presumes the wall is not a loop.+ // Wall-following mode exploits recursion to keep track of how many left turns were made+ // and then unwinds them again by ensuring each is paired with a right turn.+ // Once the recursion is fully unwound, the robot proceeds along its original direction+ // (though it may now be laterally displaced).+ //+ // (If it was a loop, then an "oriented breadcrumb" would need to be left.+ // The breadcrumb is oriented in case a single-width passage is backtracked+ // along the opposite wall.)++ /** A "gate" is walkable, so we need to supplement the "blocked" check with this function.+ Since fences are "unwalkable", they do not need to be mentioned in this function.+ */+ def isFenced =+ s <- scan forward;+ return (+ case s+ (\_. false)+ (\x. x == "gate")+ );+ end;++ def isBlockedOrFenced =+ b <- blocked;+ f <- isFenced;+ return (b || f);+ end;++ // Returns true if we've already placed two+ // breadcrumbs on a given tile, false otherwise.+ def leaveBreadcrumbs =++ let bc1 = "fresh breadcrumb" in+ let bc2 = "treaded breadcrumb" in++ wasTraversedOnce <- ishere bc1;+ if wasTraversedOnce {+ _crumb <- grab;+ make bc2;+ place bc2;+ return false;+ } {+ wasTraversedTwice <- ishere bc2;+ if wasTraversedTwice {+ return true;+ } {+ // Make sure nothing's in the way before we place+ // our breadcrumb:+ x <- scan down;+ case x return (\y.+ // If we're on a water tile, get rid of+ // it with our special "drilling" recipe+ if (y == "water") {+ drill down;+ // Nothing will remain on the ground.+ // after making the "steam" via+ // the drilling recipe.+ } {+ grab;+ return ();+ };+ );++ make bc1;+ place bc1;+ return false;+ };+ };+ end;++ def goForwardToPatrol = \wasBlocked.+ b <- isBlockedOrFenced;+ if b {+ turn left;+ goForwardToPatrol true;+ turn right;+ goForwardToPatrol false;+ } {+ if wasBlocked {+ isLoop <- leaveBreadcrumbs;+ if isLoop {+ fail "loop";+ } {};+ } {};+ move;+ };+ end;++ /**+ There should only be one place in the+ code where an exception is thrown: that is,+ if a treaded breadcrumb is encountered.+ */+ def checkIsEnclosedInner =+ try {+ goForwardToPatrol false;+ // Water is the outer boundary+ hasWater <- ishere "water";+ if hasWater {+ return false;+ } {+ checkIsEnclosedInner;+ };+ } {+ return true;+ };+ end;++ def checkIsEnclosed =++ // The "evaporator" drill is used+ // to clear water tiles.+ let specialDrill = "evaporator" in+ create specialDrill;+ equip specialDrill;++ // NOTE: System robots can walk on water+ // so we only need this if we want to+ // demo the algorithm with a player robot.+ // create "boat";+ // equip "boat";++ checkIsEnclosedInner;+ end;++ def boolToInt = \b. if (b) {return 1} {return 0}; end;++ def countAdjacentBlockages =++ turn left;+ b1 <- isBlockedOrFenced;+ c1 <- boolToInt b1;++ turn left;+ b2 <- isBlockedOrFenced;+ c2 <- boolToInt b2;++ turn left;+ b3 <- isBlockedOrFenced;+ c3 <- boolToInt b3;++ turn left;+ b4 <- isBlockedOrFenced;+ c4 <- boolToInt b4;++ return $ c1 + c2 + c3 + c4;+ end;++ // Step forward, observing left and right.+ def observeLeftAndRight =+ move;+ turn left;+ amBlockedLeft <- isBlockedOrFenced;+ val1 <- boolToInt amBlockedLeft;++ turn back;+ amBlockedRight <- isBlockedOrFenced;+ val2 <- boolToInt amBlockedRight;++ turn right;+ move;+ return $ val1 + val2;+ end;+++ /** If the four cardinal directions have at most+ one blockage, then there will exist an orientation+ where both that direction and its opposite direction+ are clear.+ So we can step that direction, check to the left and+ right of us, then step in the opposite direction+ and do the same. This allows us to check the 4+ blocks that touch the corners of the center block.+ */+ def countDiagonalBlockages =+ // First, orient to the clear front-to-back path+ amBlocked <- isBlockedOrFenced;+ if amBlocked {turn left;} {};++ // Second, step to both sides+ fwdCount <- observeLeftAndRight;+ backCount <- observeLeftAndRight;+ return $ fwdCount + backCount;+ end;++ def isStandingOnBridge =+ onFence <- ishere "fence";+ onGate <- ishere "gate";+ if (onFence || onGate) {+ adjCount <- countAdjacentBlockages;+ if (adjCount > 1) {+ return true;+ } {+ diagCount <- countDiagonalBlockages;+ return $ (adjCount + diagCount) > 1;+ };+ } {return false};+ end;++ def getValForSheepIndex = \predicateCmd. \i.+ try {+ // This will throw an exception if+ // the sheep has already drowned.+ r <- robotnumbered i;+ didSucceed <- as r {predicateCmd};++ boolToInt didSucceed;+ } {+ return 0;+ }+ end;++ /**+ There are 3 sheep.+ They have indices 1, 2, 3.+ (The base has index 0).++ THIS DOES NOT WORK!+ */+ def countSheepWithRecursive = \predicateCmd. \i.++ if (i > 0) {+ val <- getValForSheepIndex predicateCmd i;+ recursiveCount <- countSheepWithRecursive predicateCmd $ i - 1;+ return $ val + recursiveCount;+ } {+ return 0;+ }+ end;+++ def countSheepWith = \predicateCmd.++ val1 <- getValForSheepIndex predicateCmd 1;+ val2 <- getValForSheepIndex predicateCmd 2;+ val3 <- getValForSheepIndex predicateCmd 3;+ return $ val1 + val2 + val3;++ end;+++ justFilledGap <- as base {+ isStandingOnBridge;+ };++ if (justFilledGap) {+ enclosedCount <- countSheepWith checkIsEnclosed;+ return $ enclosedCount >= 1;+ } {+ return false;+ }+ - id: feed_sheep+ teaser: Feed sheep+ goal:+ - |+ Safe! Your sheep are now hungry.+ Offer them something tasty and you may be rewarded.+ - |+ The sheep will move toward something edible on+ an adjacent tile and will eat it if they walk over it.+ - |+ You may want to add a gate to the fence+ to give yourself easier access.+ condition: |-+ def getTruthForSheepIndex = \predicateCmd. \i.+ try {+ // This will throw an exception if+ // the sheep has already drowned.+ r <- robotnumbered i;+ as r {predicateCmd};+ } {+ return false;+ };+ end;++ def anySheep = \predicateCmd. \i.++ if (i > 0) {+ didSucceed <- getTruthForSheepIndex predicateCmd i;+ if didSucceed {+ return true;+ } {+ anySheep predicateCmd $ i - 1;+ };+ } {+ return false;+ };+ end;++ anySheep (has "clover") 3;+ prerequisite: enclose_sheep+ - goal:+ - |+ Yum! Contented, well-fed sheep may drop wool.+ - |+ Winter is coming! Collect three wool bundles to make a sweater.+ - |+ Each sheep drops a finite amount over+ their lifetime.+ condition: |-+ as base {+ has "sweater";+ };+ prerequisite: feed_sheep+robots:+ - name: base+ dir: [0, 1]+ devices:+ - treads+ - scanner+ - dictionary+ - branch predictor+ - strange loop+ - clock+ - ADT calculator+ - comparator+ - workbench+ - grabber+ - welder+ - lambda+ - logger+ - hearing aid+ - counter+ - mirror+ - post puller+ inventory:+ - [0, fence]+ - [0, gate]+ - [10, hinge]+ - name: sheep+ description:+ - meandering livestock+ display:+ invisible: false+ char: '@'+ system: true+ dir: [0, 1]+ inventory:+ - [4, wool]+ program: |+ run "scenarios/Challenges/Ranching/_gated-paddock/meandering-sheep.sw";+entities:+ - name: fence+ display:+ char: '#'+ description:+ - Keeps sheep in. And some other things out.+ properties: [known, portable, unwalkable]+ - name: post puller+ display:+ char: 'P'+ attr: rock+ capabilities: [drill]+ description:+ - Good for dismantling fences.+ properties: [known, portable]+ - name: scrap wood+ display:+ char: '\'+ description:+ - Scrap wood. Can be reconditioned into boards.+ properties: [known, portable]+ - name: sweater+ display:+ attr: gold+ char: 'S'+ description:+ - A warm wool sweater. Just in time for winter!+ properties: [known, portable]+ - name: clover+ display:+ attr: flower+ char: '%'+ description:+ - A tasty stack for fluffy ruminants.+ properties: [portable, growable]+ growth: [80, 100]+ - name: gate+ display:+ char: '/'+ attr: rock+ description:+ - A gate permits the player to pass through, but sheep cannot.+ properties: [known]+ - name: hinge+ display:+ char: 'U'+ attr: rock+ description:+ - Facilitates swinging action.+ properties: [known, portable]+ - name: cabin+ display:+ char: Π+ attr: rock+ description:+ - Home sweet home.+ properties: [known, unwalkable]+ - name: pier+ display:+ char: 且+ attr: rock+ description:+ - Docking area for ships+ properties: [known]+ - name: fresh breadcrumb+ display:+ char: '.'+ description:+ - A marker that can be put down and found again. Used only by judge robot.+ properties: [portable]+ - name: treaded breadcrumb+ display:+ char: 'x'+ description:+ - A marker that can be put down and found again (for a second time). Used only by judge robot.+ properties: [portable]+ - name: evaporator+ display:+ char: 'E'+ description:+ - A tool that allows clearing a water tile. Used only by judge robot.+ properties: [portable]+ capabilities: [drill]+ - name: wool+ display:+ char: 'ω'+ attr: gold+ description:+ - A bundle of raw animal fiber.+ properties: [portable]+ - name: steam+ display:+ char: 'Z'+ description:+ - What's left after evaporating water. Used only by judge robot.+ properties: [portable]+recipes:+ - in:+ - [2, board]+ out:+ - [1, fence]+ - in:+ - [3, wool]+ out:+ - [1, sweater]+ - in:+ - [1, scrap wood]+ out:+ - [1, board]+ - in:+ - [0, fresh breadcrumb]+ out:+ - [1, fresh breadcrumb]+ - in:+ - [1, fresh breadcrumb]+ out:+ - [1, treaded breadcrumb]+ - in:+ - [1, fence]+ out:+ - [1, scrap wood]+ required:+ - [1, post puller]+ - in:+ - [1, water]+ out:+ - [1, steam]+ required:+ - [1, evaporator]+ - in:+ - [1, hinge]+ - [1, fence]+ out:+ - [1, gate]+known: [mountain, tree, water]+seed: 0+solution: |+ run "scenarios/Challenges/Ranching/_gated-paddock/fence-construction.sw"+world:+ default: [dirt, water]+ palette:+ 'B': [grass, null, base]+ '.': [grass]+ 't': [dirt, tree]+ 'x': [stone, mountain]+ 'c': [stone, cabin]+ 's': [grass, null, sheep]+ '%': [grass, clover, null]+ 'H': [stone, pier, null]+ '~': [dirt, water]+ upperleft: [-34, 11]+ map: |-+ ~~~~.......~~~~~~~~~~~~~.......................~~~~~~+ ~~.............~~~~.......%.%%%%%%..%%%%%.%.......~~~+ ~........................%%%%%%%%%%%%%..%%%%.......~~+ ~...................................................~+ .............................x.xx........tttttttt...~+ .........................................tttttttt...~+ ..........................xxxxx..........tttttttt...~+ .........................................tttttttt...~+ ...........s.............................tttttttt....+ ....x....................................tttttttt....+ ...xx................................................+ ..xx.....................................B......c....+ ...x.................................................+ ..............................s......................+ .....................................................+ ....................................................~+ ~..............s...................................~~+ ~~.......xxx...........x.........................~~~~+ ~~~............xx............x............~~~~H~~~~~~+ ~~~.....................................~~~~~~H~~~~~~+ ~~.....................................~~~~~~~H~~~~~~+ ~~..............................~~~~~~~~~~~~~~H~~~~~~+ ~~~~...................~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ data/scenarios/Challenges/_bucket-brigade/brigade.sw view
@@ -0,0 +1,94 @@+def doN = \n. \f. if (n > 0) {f; doN (n - 1) f} {}; end;++def forever = \c. c; forever c; end;++def pollUntilHas = \item.+ itemInInventory <- has item;+ if itemInInventory {} {+ pollUntilHas item;+ };+ end;++def pollUntilCount = \targetCount. \item.+ itemCount <- count item;+ if (itemCount < targetCount) {+ pollUntilCount targetCount item;+ } {};+ end;++def waitToGiveUntilHas = \item. \recipient.+ pollUntilHas item;+ give recipient item;+ end;++/** The "depth" is the number of spaces between+the root robot and the target item.+It is one minus the distance to the target item.+*/+def recurseUntilDepth = \depth.+ let item = "coal lump" in+ child <- build {++ // NOTE: Treads are auto-equipped via the implicit "require"+ move;+ unequip "treads";++ pollUntilCount depth "repro kit";++ // Use the recipe from "repro kit"+ make "3D printer";++ equip "solar panel";+ equip "grabber";+ equip "3D printer";+ equip "dictionary";+ equip "branch predictor";+ equip "comparator";+ equip "calculator";+ equip "lambda";+ equip "strange loop";+ equip "string";+ equip "clock";+ equip "counter";+ equip "mirror";+ equip "scanner";+ equip "logger";+ equip "net";++ if (depth > 1) {+ recurseUntilDepth $ depth - 1;+ forever $ waitToGiveUntilHas item parent;+ } {+ make "bucketwheel excavator";+ forever (+ drill forward;+ give parent item;+ );+ };+ };++ let kitGiftCount = depth in+ // say $ "Going to give child " ++ (format kitGiftCount) ++ " repro kits.";+ doN kitGiftCount $ give child "repro kit";++ end;++def makeBriquette =+ let briquette = "coal briquette" in+ pollUntilCount 100 "coal lump";+ make briquette;+ briquetteAlreadyPlaced <- ishere briquette;+ if briquetteAlreadyPlaced {} {+ place briquette;+ };+ end;++def go =+ // Use the recipe from "repro kit" to unpack the right+ // set of items to be taken from the inventory with the implicit "require":+ make "3D printer";+ recurseUntilDepth 8;+ forever makeBriquette;+ end;++go;
+ data/scenarios/Challenges/_bucket-brigade/hauler.sw view
@@ -0,0 +1,44 @@+def doN = \n. \f. if (n > 0) {f; doN (n - 1) f} {}; end;++/** The function argument returns a boolean.+Keep running the function until either the+loop counter runs out or the function returns true.+*/+def doNTimesOr = \n. \f.+ if (n > 0) {+ shouldQuit <- f;+ if shouldQuit {} {+ doN (n - 1) f;+ }+ } {};+ end;++def forever = \c. c; forever c; end;++def traverseRoad =+ doN 11 (+ move;+ wait 7;+ );+ end;++def patrol =+ let briquette = "coal briquette" in+ traverseRoad;+ doNTimesOr 40 (+ briquetteIsHere <- ishere briquette;+ if briquetteIsHere {grab; return ()} {};+ return briquetteIsHere;+ );+ turn back;+ traverseRoad;++ hasBriquette <- has briquette;+ if hasBriquette {+ say "Delivered!";+ make "energy";+ } {};+ turn back;+ end;++forever patrol;
+ data/scenarios/Challenges/_bucket-brigade/powerplant.sw view
@@ -0,0 +1,20 @@+def doN = \n. \f. if (n > 0) {f; doN (n - 1) f} {}; end;++def flashLights =+ doN 8 (+ move;+ wait 8;+ turn back;+ );+ end;++def checkForEnergy = \lastCount.+ hauler <- robotnamed "hauler";+ energyCount <- as hauler {+ count "energy";+ };+ if (energyCount > lastCount) {flashLights} {};+ checkForEnergy energyCount;+ end;++checkForEnergy 0;
+ data/scenarios/Challenges/_friend/cat.sw view
@@ -0,0 +1,75 @@+def forever : cmd unit -> cmd unit = \c. c ; forever c end++def repeat : int -> cmd unit -> cmd unit =+ \n. \c. if (n == 0) {} {c ; repeat (n-1) c}+end++def elif = \t. \then. \else. {if t then else} end+def else = \t. t end++def abs = \n. if (n < 0) {-n} {n} end++def randdir : cmd dir =+ d <- random 4;+ return (+ if (d == 0) {north}+ $ elif (d == 1) {east}+ $ elif (d == 2) {south}+ $ else {west}+ )+end++def chooseWait : cmd int =+ t <- random (16*2);+ return (16 + t)+end++def wander =+ d <- randdir;+ turn d;+ dist <- random 2;+ try {repeat dist move} {};+ r <- random 5;+ if (r == 0) { say "meow" } {}+end++def disappointed = \cat. say "meow??"; cat end++def follow : cmd unit -> actor -> cmd unit = \cat. \r.+ rLoc <- as r {whereami};+ myLoc <- whereami;+ let dx = fst rLoc - fst myLoc in+ let dy = snd rLoc - snd myLoc in+ if (abs dx > abs dy)+ { if (dx < 0) {turn west} {turn east} }+ { if (dy < 0) {turn south} {turn north} };+ if (abs dx != 0 || abs dy != 0) {try { move } { disappointed cat }} {};+ wait 4;+ follow cat r+end++def love = \cat.+ say "purr";+ fishGiver <- meet;+ case fishGiver+ (\_. disappointed cat)+ (\r. follow cat r)+end++def cat = \start. \fishCount. \waitTime.+ if (waitTime == 0) { wander; start } { wait 1 };+ n <- count "fish";+ if (n > fishCount)+ { say "yum!";+ if (n >= 3) { love start } { cat start n (waitTime - 1) }+ }+ { cat start fishCount (waitTime - 1) }+end++def startCat =+ n <- count "fish";+ w <- chooseWait;+ cat startCat n w+end;++startCat
+ data/scenarios/Challenges/_friend/friend-solution.sw view
@@ -0,0 +1,18 @@+def m2 = move; move end+def m5 = m2; m2; move end+def m10 = m5; m5 end+def m20 = m10; m10 end++def give_fish = \n.+ if (n == 0) {}+ { mcat <- meet;+ case mcat (\_. give_fish n) (\cat. give cat "fish"; give_fish (n-1))+ }+end;++build {+ require 3 "fish";+ m2; turn left; m20;+ give_fish 3;+ turn back; m20; turn right; m2+}
+ data/scenarios/Challenges/_ice-cream/customer.sw view
@@ -0,0 +1,60 @@+def doN = \n. \f. if (n > 0) {f; doN (n - 1) f} {}; end;++def approachShop =+ doN 5 (+ move;+ wait 8;+ );+ end;++def placeOrder =+ scoopCountExtra <- random 4000;+ let scoopCount = 1000 + scoopCountExtra in+ say $ format scoopCount ++ " scoops of vanilla ice cream, no more, no less! And top it with a cherry, please.";+ return scoopCount;+ end;++def waitForItem = \item.+ hasItem <- has item;+ if hasItem {+ return ();+ } {+ wait 1;+ waitForItem item;+ }+ end;++/**+If we receive the cherry+when we also have the correct number of+scoops, then we produce this item+as a sempahore for the goal evaluation.++Otherwise, we express our disappointment.+*/+def createCompletedSemaphore = \targetCount.+ scoopcount <- count "scoop";+ if (scoopcount < targetCount) {+ say "That's not enough ice cream! Terrible service.";+ } {+ if (scoopcount > targetCount) {+ say "That's too much ice cream!";+ } {+ say "Just right! Here's your money.";+ give base "bitcoin";+ };+ };+ turn back;+ approachShop;+ selfdestruct;+ end;++def runScript =+ approachShop;+ waitForItem "cone";+ targetScoopCount <- placeOrder;+ waitForItem "cherry";+ createCompletedSemaphore targetScoopCount;+ end;++runScript;
+ data/scenarios/Challenges/_ice-cream/solution.sw view
@@ -0,0 +1,68 @@+def serveCustomer = \cone. \customer.+ give customer cone;+ end;++def getCounter =+ move;+ c <- grab;+ equip c;+ end;++def getBriefcase =+ turn left;+ move;+ move;+ b <- grab;+ equip b;+ turn back;+ move;+ move;+ end;++def getIngredients =+ move;+ move;+ cone <- grab;+ move;+ cherry <- grab;+ turn back;+ move;+ move;+ move;+ turn right;+ return (cone, cherry);+ end;++def meetCustomer =+ maybeCustomer <- meet;+ case maybeCustomer (\_. meetCustomer) return;+ end;++def serveScoop = \customer.+ let s = "scoop" in+ make s;+ give customer s;+ end;++def scoopUntil = \customer. \targetRemainingIngredientCount.+ currentCount <- count "milk";+ if (currentCount > targetRemainingIngredientCount) {+ serveScoop customer;+ scoopUntil customer targetRemainingIngredientCount;+ } {};+ end;++def runSolution = \targetRemainingIngredientCount.+ getBriefcase;+ coneAndCherry <- getIngredients;+ getCounter;+ customer <- meetCustomer;++ let cone = fst coneAndCherry in+ let cherry = snd coneAndCherry in+ serveCustomer cone customer;+ scoopUntil customer targetRemainingIngredientCount;+ give customer cherry;+ end;++runSolution 2712;
+ data/scenarios/Challenges/_wolf-goat-cabbage/cross.sw view
@@ -0,0 +1,40 @@+def doN = \n. \f. if (n > 0) {f; doN (n - 1) f} {}; end;++def crossRiver =+ doN 18 move;+ end;++move;+move;+goat <- grab;+turn east;+crossRiver;+place goat;+turn back;+crossRiver;+turn right;+move;+wolf <- grab;+turn right;+crossRiver;+place wolf;+turn right;+move;+goat2 <- grab;+turn right;+crossRiver;+place goat2;+turn left;+move;+cabbage <- grab;+turn left;+crossRiver;+place cabbage;+turn back;+crossRiver;+turn right;+move;+goat3 <- grab;+turn right;+crossRiver;+place goat3;
+ data/scenarios/Challenges/_wolf-goat-cabbage/multi-item-possession.sw view
@@ -0,0 +1,4 @@+has_wolf <- has "wolf";+has_goat <- has "goat";+has_cabbage <- has "cabbage";+return $ (has_wolf && has_goat) || (has_goat && has_cabbage) || (has_wolf && has_cabbage);
+ data/scenarios/Challenges/_wolf-goat-cabbage/together-on-east-bank.sw view
@@ -0,0 +1,28 @@+def get_x_coord = \r.+ as r {+ pos <- whereami;+ return $ fst pos;+ };+ end;++def all_on_bank = \baseX. \robotName.+ r <- robotnamed robotName;+ thisX <- get_x_coord r;++ as r {+ try {grab; return ()} {};+ move;+ try {grab; return ()} {};+ move;+ try {grab; return ()} {};++ has_wolf <- has "wolf";+ has_goat <- has "goat";+ has_cabbage <- has "cabbage";+ return $ baseX == thisX && has_wolf && has_goat && has_cabbage;+ };+ end;++baseX <- get_x_coord base;++all_on_bank baseX "east_detector";
+ data/scenarios/Challenges/_wolf-goat-cabbage/unattended-together.sw view
@@ -0,0 +1,35 @@+def get_x_coord = \r.+ as r {+ pos <- whereami;+ return $ fst pos;+ };+ end;++def is_unattended_together = \baseX. \robotName.+ r <- robotnamed robotName;+ thisX <- get_x_coord r;++ if (baseX == thisX) {+ return false;+ } {+ as r {+ try {grab; return ()} {};+ move;+ try {grab; return ()} {};+ move;+ try {grab; return ()} {};++ has_wolf <- has "wolf";+ has_goat <- has "goat";+ has_cabbage <- has "cabbage";+ return $ (has_wolf && has_goat) || (has_goat && has_cabbage);+ }; + }+ end;++baseX <- get_x_coord base;++west_bad <- is_unattended_together baseX "west_detector";+east_bad <- is_unattended_together baseX "east_detector";++return $ west_bad || east_bad;
+ data/scenarios/Challenges/_word-search/create-puzzle.sw view
@@ -0,0 +1,247 @@+def doN = \n. \f. if (n > 0) {f; doN (n - 1) f} {}; end;++def intersperse = \n. \f2. \f1. if (n > 0) {+ f1;+ if (n > 1) {+ f2;+ } {};+ intersperse (n - 1) f2 f1;+ } {};+ end;++def whichOrdinal = \str.+ if (str == "capital C") {+ return 0;+ } {+ if (str == "capital O") {+ return 1;+ } {+ if (str == "capital W") {+ return 2;+ } {+ return (-1);+ }+ }+ }+ end;++/**+Returns -1 if not a recognized letter.+*/+def getAdjacentOrdinal = \d.+ maybeEntity <- scan d;+ str <- case maybeEntity (\_. return "") (\s. return s);+ whichOrdinal str;+ end;++/**+Call a function repeatedly with the numeric argument,+in descending order.+*/+def iterN = \n. \f.+ if (n > 0) {+ let newNum = n - 1 in+ f newNum;+ iterN newNum f;+ } {};+ end;++def chooseLetter = \i.+ if (i == 0) {+ return "capital C";+ } {+ if (i == 1) {+ return "capital O";+ } {+ return "capital W";+ }+ };+ end;++/**+We only need to probe further if+the letter above us is an O.+Then we need to check whether the letter+above that is a C or a W.+*/+def getExcludedVerticalLetter =+ northOrdinal <- getAdjacentOrdinal north;+ if (northOrdinal == 1) {+ currentLoc <- whereami;+ teleport self (fst currentLoc, snd currentLoc + 1);+ doubleNorthOrdinal <- getAdjacentOrdinal north;+ teleport self currentLoc;++ if (doubleNorthOrdinal == 2) {+ return 0;+ } {+ if (doubleNorthOrdinal == 0) {+ return 2;+ } {+ return (-1);+ }+ }+ } {+ return (-1);+ }+ end;++/**+To ensure there are limited numbers of solutions+(preferably exactly one),+make sure we're not completing a word+horizontally (foward or backward)+or vertically (upward or downward), except if we+are in the designated location.+*/+def reRoll = \excludedVertical. \expectedFwdOrdinal. \expectedBkwdOrdinal.++ // NOTE: excludeTwo and excludeZero+ // are mutually exclusive!+ let excludeTwo = expectedFwdOrdinal == 2 in+ let excludeZero = expectedBkwdOrdinal == 0 in++ // NOTE: Excluded letters can only be 0 or 2.+ // 1 is always a valid option.++ if excludeZero {+ if (excludedVertical == 2) {+ return 1;+ } {+ // Zero is the only excluded value,+ // so just offset a choice between 0 and 1 upward by 1, + // to make it a choice between 1 and 2.+ val <- random 2;+ return $ val + 1;+ };+ } {+ if excludeTwo {+ if (excludedVertical == 0) {+ return 1;+ } {+ // Two is the only excluded value,+ // so make it a choice between 0 and 1.+ random 2;+ };+ } {+ if (excludedVertical == 0) {+ // Zero is the only excluded value,+ // so just offset a choice between 0 and 1 upward by 1, + // to make it a choice between 1 and 2.+ val <- random 2;+ return $ val + 1;+ } {+ if (excludedVertical == 2) {+ // Two is the only excluded value,+ // so make it a choice between 0 and 1.+ random 2;+ } {+ // No values are excluded, so select random from+ // the full range.+ random 3;+ }+ };+ };+ };+ end;++def singleTile = \expectedFwdOrdinal. \expectedBkwdOrdinal.+ excludedVertical <- getExcludedVerticalLetter;+ letterIndex <- reRoll excludedVertical expectedFwdOrdinal expectedBkwdOrdinal;+ chosenLetter <- chooseLetter letterIndex;+ place chosenLetter;+ return letterIndex;+ end;++def crossBack = \n.+ currentLoc <- whereami;+ teleport self (0, snd currentLoc - 1);+ end;++/**+Recursive. Tracks the completion of the word in both the forward+and backward directions.+*/+def layTilesRow = \expectedFwdOrdinal. \expectedBkwdOrdinal. \n.+ placedIndex <- singleTile expectedFwdOrdinal expectedBkwdOrdinal;++ if (n > 1) {+ move;++ newFwdOrdinal <- if (placedIndex == expectedFwdOrdinal || placedIndex == 0) {+ return $ placedIndex + 1;+ } {+ return 0;+ };++ newBkwdOrdinal <- if (placedIndex == expectedBkwdOrdinal || placedIndex == 2) {+ return $ placedIndex - 1;+ } {+ return 2;+ };++ layTilesRow newFwdOrdinal newBkwdOrdinal $ n - 1;+ } {};+ end;++def giveLetterNumbered = \n.+ letter <- chooseLetter n;+ give base letter;+ end;++def removeBoulder =+ baseLoc <- as base {whereami};+ teleport self (fst baseLoc - 1, snd baseLoc);++ // Remove the boulder blocking the player's path+ grab;++ // Make sure the base "knows" about the letters+ // to get rid of the question marks ("?").+ iterN 3 giveLetterNumbered;++ selfdestruct;+ end;++def createImpossiblePuzzle = \width. \height.+ intersperse height (crossBack width) (layTilesRow 0 2 width);+ end;++/**+This word center is padded by+one cell from the vertical and horizontal+edges of the playfield.+This means that the word will not exist+horizontally at either the top or bottom edge,+nor will it exist vertically at the left or+right edge of the playfield.+*/+def overwriteWithWord = \width. \height.++ randX <- random $ width - 2;+ randY <- random $ height - 2;+ let insertionX = 1 + randX in+ let insertionY = 1 + randY in+ teleport self (insertionX, -insertionY);++ // Rotate to random orientation+ turnCount <- random 3;+ doN turnCount $ turn left;++ move;+ turn back;++ // Place all three letters of the word+ iterN 3 $ \x.+ chosenLetter <- chooseLetter x;+ swap chosenLetter;+ move;+ end;++def createPuzzle = \width. \height.+ createImpossiblePuzzle width height;+ overwriteWithWord width height;+ removeBoulder;+ end;++createPuzzle 25 15;
+ data/scenarios/Challenges/_word-search/solution.sw view
@@ -0,0 +1,127 @@+def doN = \n. \f. if (n > 0) {f; doN (n - 1) f} {}; end;++def intersperse = \n. \f2. \f1. if (n > 0) {+ f1;+ if (n > 1) {+ f2;+ } {};+ intersperse (n - 1) f2 f1;+ } {};+ end;++def waitUntilUnblocked =+ x <- blocked;+ if x {+ wait 1;+ waitUntilUnblocked;+ } {};+ end;++def whichOrdinal =+ isC <- ishere "capital C";+ if (isC) {+ return 0;+ } {+ isO <- ishere "capital O";+ if (isO) {+ return 1;+ } {+ isW <- ishere "capital W";+ if (isW) {+ return 2;+ } {+ return (-1);+ }+ }+ }+ end;++// Go to upper-left corner+def goToCorner =+ myLoc <- whereami;+ doN (fst myLoc) move;+ turn right;+ doN (-(snd myLoc)) move;+ turn right;+ end;++def highlightLetter =+ drill down;+ end;++def traverseRow = \expectedOrdinal. \colCount.++ theFoundOrdinal <- whichOrdinal;++ // Logic: the first letter of the target word is *always*+ // considered a "match".+ let shouldAdvance = theFoundOrdinal == expectedOrdinal || theFoundOrdinal == 0 in+ newExpectedOrdinal <- if shouldAdvance {+ return $ theFoundOrdinal + 1;+ } {+ // Reset the progress+ return 0;+ };++ if (newExpectedOrdinal == 3) {+ turn back;++ intersperse 3 move highlightLetter;+ return true;+ } {+ if (colCount > 1) {+ move;+ traverseRow newExpectedOrdinal (colCount - 1);+ } {+ return false;+ };+ };+ end;++def advanceRow =+ turn left;+ move;+ turn left;+ end;++/**+Travels forward and then backward+across a row, to check for solutions+in either direction.+*/+def traverseCols = \width. \height. + didWin <- traverseRow 0 width;+ if didWin {+ return true;+ } {+ turn back;+ didWinBackward <- traverseRow 0 width;+ if didWinBackward {+ return true;+ } {+ if (height > 1) {+ advanceRow;+ traverseCols width $ height - 1;+ } {+ return false;+ };+ }+ } + end;++def solve = \boardWidth. \boardHeight.+ waitUntilUnblocked;+ goToCorner;++ wonHorizontally <- traverseCols boardWidth boardHeight;+ if wonHorizontally {+ return true;+ } {+ // If we did not find a horizontal solution,+ // look for vertical solutions.+ turn right;+ traverseCols boardHeight boardWidth;+ }+ end;++solve 25 15;
+ data/scenarios/Challenges/_word-search/verify-solution.sw view
@@ -0,0 +1,183 @@+/**+Algorithm:+We only need to check the base's+current position: if we find three contiguous highlights,+then we know that the player has just completed their+third highlight.+*/++def whichOrdinal =+ isC <- ishere "lowercase c";+ if (isC) {+ return 0;+ } {+ isO <- ishere "lowercase o";+ if (isO) {+ return 1;+ } {+ isW <- ishere "lowercase w";+ if (isW) {+ return 2;+ } {+ return (-1);+ }+ }+ }+ end;+++def whichHighlightedOrdinal = \str.+ if (str == "lowercase c") {+ return 0;+ } {+ if (str == "lowercase o") {+ return 1;+ } {+ if (str == "lowercase w") {+ return 2;+ } {+ return (-1);+ }+ }+ }+ end;++def countConsecutive = \expectedOrdinal. \n.++ thisOrdinal <- whichOrdinal;+ nextOrdinal <- if (thisOrdinal == expectedOrdinal) {+ return $ expectedOrdinal + 1;+ } {+ return 0;+ };++ if (nextOrdinal == 3) {+ return true;+ } {+ if (n > 1) {+ move;+ countConsecutive nextOrdinal $ n - 1;+ } {+ return false;+ };+ };+ end;++def checkBackAndForth =++ foundBackward <- countConsecutive 0 3;+ if (foundBackward) {+ return true;+ } {+ turn back;+ countConsecutive 0 3;+ }+ end;++def checkDirections = \n.+ if (n > 0) {+ wasFound <- checkBackAndForth;+ if wasFound {+ return true;+ } {+ turn left;+ checkDirections $ n - 1;+ }+ } {+ return false;+ }+ end;++def isMarkedInDirection = \d.+ scanResult <- scan d;+ ordinalNum <- case scanResult+ (\_. return (-1))+ whichHighlightedOrdinal;+ return $ ordinalNum >= 0;+ end;++/**+It's possible we could be one cell away from+a marked cell after finishing, either due+to using a directional `drill` command instead of+`drill down`, or due to an apparent bug which+does not evaluate the goal condition between the+`drill` and a `move` command.+*/+def moveToMarkedCell = \n.+ if (n > 0) {+ isMarkedAhead <- isMarkedInDirection forward;+ if isMarkedAhead {+ move;+ return true;+ } {+ turn left;+ moveToMarkedCell $ n - 1;+ };+ } {+ return false;+ };+ end;++/**+Orient ourselves such that+a marked cell is behind us.+*/+def findMarkBehind = \n.+ if (n > 0) {+ isMarkedBehind <- isMarkedInDirection back;+ if isMarkedBehind {+ return true;+ } {+ turn left;+ findMarkBehind $ n - 1;+ };+ } {+ return false;+ };+ end;++/**+The cell we're on might be in the middle of a word,+rather than the end. Determine the orientation of+the line, then move along it until reaching the end.++Algorithm:+0. Assumption: we are currently on a marked cell.+1. Turn in all all four directions to `scan back`+ for a second marked cell. Stop turning if+ we encounter one.+ If none found after 4 turns, abort.+2. `scan forward` to see if there is a marked cell in+ the opposite direction.+ `move` (foward) once if there is. Since the word+ is only three cells long, this will be the other+ end of it.+*/+def moveToWordExtrema =+ foundCellBehind <- findMarkBehind 4;+ if foundCellBehind {+ isMarkedAhead <- isMarkedInDirection forward;+ if isMarkedAhead {+ move;+ } {};+ } {};+ end;++def checkSoln =+ isMarkedHere <- isMarkedInDirection down;+ atMarkedCell <- if isMarkedHere {+ return true;+ } {+ moveToMarkedCell 4;+ };++ if atMarkedCell {+ moveToWordExtrema;+ checkDirections 4;+ } {+ return false;+ }+ end;++as base {checkSoln};
+ data/scenarios/Challenges/bucket-brigade.yaml view
@@ -0,0 +1,216 @@+version: 1+name: Bucket Brigade+author: Karl Ostmo+description: |+ Convey resources with limited mobility+creative: false+robots:+ - name: base+ display:+ char: 'Ω'+ attr: robot+ heavy: true+ dir: [0, 1]+ devices:+ - 3D printer+ - ADT calculator+ - branch predictor+ - clock+ - comparator+ - counter+ - dictionary+ - grabber+ - hearing aid+ - lambda+ - logger+ - mirror+ - net+ - scanner+ - strange loop+ - string+ - workbench+ - briquette press+ inventory:+ - [10, repro kit]+ - [1, treads]+ - name: hauler+ system: true+ dir: [-1, 0]+ devices:+ - furnace+ display:+ invisible: false+ char: '='+ attr: robot+ program: |+ run "scenarios/Challenges/_bucket-brigade/hauler.sw"+ - name: powerplant+ system: true+ dir: [1, 0]+ display:+ invisible: false+ char: '▒'+ attr: gold+ program: |+ run "scenarios/Challenges/_bucket-brigade/powerplant.sw"+objectives:+ - id: deliver_coal_lump+ teaser: Get coal to base+ goal:+ - Deliver a "coal lump" to the base.+ - |+ To excavate coal from the "lignite mine" (M), a robot needs to+ `drill` while in posession of a "bucketwheel excavator".+ - |+ To assemble the excavator, you'll need to repurpose+ some "treads".+ Unfortunately, you have only one set of "treads".+ You'll have to make do...+ condition: |+ as base {has "coal lump"}+ - goal:+ - Your base got some coal!+ - Now fashion more of it into a "coal briquette", and place it+ so that the hauler (=) can pick it up.+ - The hauler will periodically stop at the base+ to check if a load is available.+ condition: |+ hauler <- robotnamed "hauler";+ as hauler {has "coal briquette"}+ prerequisite: deliver_coal_lump+solution: |+ run "scenarios/Challenges/_bucket-brigade/brigade.sw"+entities:+ - name: bucketwheel excavator+ display:+ char: 'x'+ description:+ - A powerful machine designed to remove overburden in mining operations.+ properties: [known, portable]+ capabilities: [drill]+ - name: lignite mine+ display:+ attr: copper+ char: 'M'+ description:+ - Requires heavy equipment to remove the overburden.+ properties: [known]+ - name: coal lump+ display:+ char: 'c'+ description:+ - Primitive, sooty fuel.+ properties: [known, portable]+ - name: coal briquette+ display:+ char: 'q'+ description:+ - A compressed, convenient form of coal for consumption and transport.+ properties: [known, portable]+ - name: briquette press+ display:+ char: 'P'+ description:+ - A device to compress coal lumps into briquettes.+ properties: [known, portable]+ - name: energy+ display:+ char: 'E'+ description:+ - The result of burning a coal briquettte.+ properties: [portable]+ - name: repro kit+ display:+ char: 'k'+ description:+ - Kit that can be unpacked into everything a robot needs to reproduce.+ properties: [known, portable]+recipes:+ - in:+ - [1, repro kit]+ out:+ - [1, 3D printer]+ - [1, ADT calculator]+ - [1, branch predictor]+ - [1, calculator]+ - [1, clock]+ - [1, comparator]+ - [1, counter]+ - [1, dictionary]+ - [1, grabber]+ - [1, lambda]+ - [1, lodestone]+ - [1, logger]+ - [1, metal drill]+ - [1, mirror]+ - [1, net]+ - [1, scanner]+ - [1, solar panel]+ - [1, string]+ - [1, strange loop]+ - [1, welder]+ - [1, workbench]+ - in:+ - [1, treads]+ out:+ - [1, bucketwheel excavator]+ - in:+ - [1, lignite mine]+ out:+ - [1, lignite mine]+ - [1, coal lump]+ required:+ - [1, bucketwheel excavator]+ - [1, metal drill]+ - in:+ - [100, coal lump]+ out:+ - [1, coal briquette]+ required:+ - [1, briquette press]+ - in:+ - [1, coal briquette]+ out:+ - [1, energy]+ required:+ - [1, furnace]+++known: [boulder, lignite mine]+seed: 0+world:+ default: [grass]+ palette:+ 'B': [dirt, null, base]+ '.': [dirt]+ '=': [stone]+ 'M': [stone, lignite mine]+ 'H': [stone, null, hauler]+ 'Z': [stone, null, powerplant]+ 'w': [grass]+ 's': [stone]+ 'A': [stone, boulder]+ '┌': [stone, upper left corner]+ '┐': [stone, upper right corner]+ '└': [stone, lower left corner]+ '┘': [stone, lower right corner]+ '─': [stone, horizontal wall]+ '│': [stone, vertical wall]+ upperleft: [0, 0]+ map: |-+ ww...A.AA.A..A.AA....A..ww+ w..A..A.AA........A......w+ .AA..A......M....A....A...+ ....A..A..................+ .A...A.A..................+ ..AA......................+ ...A..A...............┌┐..+ ..A.................┌┐││..+ .....A.............┌────┐.+ ...................│sZss│.+ .A.................└────┘.+ .....A......B==========H=.+ ..........................+ .............┌┐.┌──────┐..+ w......A.....└┘.└──────┘..+ ww........................
+ data/scenarios/Challenges/friend.yaml view
@@ -0,0 +1,94 @@+version: 1+name: Make a Friend+author: Brent Yorgey+description: |+ Win the trust of a furry companion.+creative: false+robots:+ - name: base+ display:+ char: 'Ω'+ attr: robot+ heavy: true+ dir: [0, 1]+ devices:+ - 3D printer+ - ADT calculator+ - antenna+ - branch predictor+ - clock+ - comparator+ - counter+ - dictionary+ - grabber+ - hearing aid+ - lambda+ - logger+ - mirror+ - net+ - scanner+ - strange loop+ - string+ - toolkit+ - workbench+ inventory:+ - [10, ADT calculator]+ - [10, antenna]+ - [10, treads]+ - [10, branch predictor]+ - [10, fish]+ - [10, solar panel]+ - [10, grabber]+ - [10, lambda]+ - [10, strange loop]+ - [10, logger]+ - [10, clock]+ - [10, comparator]+ - [10, calculator]+ - name: cat+ system: true+ dir: [-1, 0]+ display:+ invisible: false+ char: 'c'+ attr: sand+ devices:+ - logger+ program: |+ run "scenarios/Challenges/_friend/cat.sw"+objectives:+ - goal:+ - There's a cat wandering around in the field. Bring it back to+ your base. If you give it something it likes, perhaps you can+ get it to follow you.+ condition: |+ c <- robotNamed "cat";+ catLoc <- as c {whereami};+ baseLoc <- as base {whereami};+ return (catLoc == baseLoc)+solution: |+ run "scenarios/Challenges/_friend/friend-solution.sw"+entities:+ - name: fish+ display:+ char: 'f'+ description:+ - A smelly fish. Rather unappetizing to a robot.+ properties: [known, portable]++known: [fish]+seed: 0+world:+ offset: true+ palette:+ 'Ω': [grass, null, base]+ '.': [stone]+ ',': [grass]+ 'c': [grass, null, cat]+ '*': [grass, flower]+ '@': [stone, boulder]+ upperleft: [-20, 2]+ map: |-+ c,..,,,,,,..,,,,...,.+ ,..,,,,,,,...........+ *,.,@,,,,,,,.....,..Ω
data/scenarios/Challenges/hanoi.yaml view
@@ -57,7 +57,7 @@ // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // if // 0. I stand on unlocked X- // 1. place noth of me is NOT empty+ // 1. place north of me is NOT empty // then // - lock X run "scenarios/Challenges/hanoi-invariant.sw"@@ -157,7 +157,7 @@ world: default: [grass, null] palette:- ' ': [grass]+ ',': [grass] '_': [stone] 'v': [stone, null, base] '┌': [stone, upper left corner]@@ -182,5 +182,5 @@ │2│.│.│ │3│.│.│ └─┴─┴─┘- ^ ^ ^ - X + ,^,^,^,+ ,,,X,,,
+ data/scenarios/Challenges/ice-cream.yaml view
@@ -0,0 +1,241 @@+version: 1+name: Ice Cream Shop+author: Karl Ostmo+description: |+ Serve a demanding customer+creative: false+seed: 0+objectives:+ - goal:+ - |+ Congratulations on the grand opening of your new ice cream shop.+ You have advertised: "All you can eat, for 1 bitcoin!"+ - |+ A customer is approaching the "Counter". They look hungry!+ - "..."+ - |+ Customer: "`give` me a cone, and then I'll tell you how many scoops I want."+ - "..."+ - |+ Oh dear, you've forgotten to stock your shop with a "calculator".+ Let's hope this order is simple.+ condition: |+ customer <- robotnamed "customer";+ as customer {has "cone"};+ - goal:+ - |+ `give` the customer exactly what they request.+ - |+ Perhaps there is something else around the shop that can assist with+ preparing the order...+ condition: |+ as base {+ has "bitcoin";+ };+robots:+ - name: base+ dir: [0, 1]+ devices:+ - branch predictor+ - treads+ - antenna+ - comparator+ - workbench+ - grabber+ - dictionary+ - lambda+ - logger+ - welder+ - hearing aid+ - scanner+ - strange loop+ inventory:+ - [5000, egg]+ - [5000, milk]+ - [5000, sugar]+ - [5000, ice]+ - [5000, vanilla]+ - name: customer+ system: true+ dir: [0, -1]+ display:+ invisible: false+ attr: 'iron'+ inventory:+ - [1, bitcoin]+ program: |+ run "scenarios/Challenges/_ice-cream/customer.sw"+solution: |+ run "scenarios/Challenges/_ice-cream/solution.sw"+entities:+ - name: scoop+ display:+ char: '@'+ description:+ - A single scoop of vanilla ice cream+ properties: [portable]+ - name: cherry+ display:+ char: '6'+ attr: 'red'+ properties: [portable]+ description:+ - A traditional ice cream garnish+ - name: Counter+ display:+ char: '▒'+ attr: 'sand'+ properties: [portable, known]+ capabilities: [count]+ description:+ - Where customers are served.+ - Also... it can count things.+ - name: cone+ display:+ char: 'V'+ attr: 'sand'+ properties: [portable]+ description:+ - A conical, edible container for ice cream+ - name: egg+ display:+ char: 'e'+ attr: 'gold'+ properties: [portable]+ description:+ - Organic from local, free-range, meticulously pampered chickens.+ - name: milk+ display:+ char: 'm'+ attr: 'silver'+ properties: [portable]+ description:+ - Pasteurized, homogenized, commoditized, metabolized.+ - name: ice+ display:+ char: 'i'+ attr: 'snow'+ properties: [portable]+ description:+ - Ice cream is better cold, don't you think?+ - name: sugar+ display:+ char: 's'+ attr: 'snow'+ properties: [portable]+ description:+ - Basis of most desserts+ - name: vanilla+ display:+ char: 'v'+ attr: 'wood'+ properties: [portable]+ description:+ - Traditional ice cream flavoring+ - name: briefcase+ display:+ char: 'B'+ attr: rubber+ capabilities: [sum, prod]+ properties: [portable]+ description:+ - Standard business accoutrement.+ - Particularly useful for `meet`-ings.+ - Enables the `case` expression.+ - name: awning1+ display:+ char: '▒'+ attr: flower+ description:+ - Decoration+ properties: [known]+ - name: awning2+ display:+ char: '▒'+ attr: snow+ description:+ - Decoration+ properties: [known]+ - name: cone left+ display:+ char: '\'+ attr: wood+ description:+ - Decoration+ properties: [known]+ - name: cone right+ display:+ char: '/'+ attr: wood+ description:+ - Decoration+ properties: [known]+ - name: cone top+ display:+ char: 'M'+ attr: wood+ description:+ - Decoration+ properties: [known]+ - name: cone inside+ display:+ char: 'Z'+ attr: wood+ description:+ - Decoration+ properties: [known]+ - name: decorative scoop+ display:+ char: 'O'+ attr: snow+ description:+ - Decoration+ properties: [known]+recipes:+ - in:+ - [1, egg]+ - [1, milk]+ - [1, sugar]+ - [1, ice]+ - [1, vanilla]+ out:+ - [1, scoop]+known: []+world:+ default: [grass]+ upperleft: [0, 0]+ offset: false+ palette:+ '@': [stone, boulder]+ '.': [grass]+ ':': [stone]+ 'B': [stone, briefcase]+ '6': [stone, cherry]+ 'V': [stone, cone]+ Ω: [stone, null, base]+ X: [stone, null, customer]+ 'y': [stone, awning1]+ 'z': [stone, awning2]+ '┌': [stone, upper left corner]+ '┐': [stone, upper right corner]+ '└': [stone, lower left corner]+ '┘': [stone, lower right corner]+ '─': [stone, horizontal wall]+ '│': [stone, vertical wall]+ '\': [stone, cone left]+ '/': [stone, cone right]+ 'Z': [stone, cone inside]+ '^': [stone, cone top]+ 'O': [stone, decorative scoop]+ 'x': [stone, Counter]+ map: |+ ...X..........+ ...:..........+ ...:..........+ ...:..........+ ...:.......OO.+ yzy:yzyz..OOOO+ ┌─┐x┌──┐..^^^^+ │B:Ω:V6│..\ZZ/+ └──────┘...\/.+
data/scenarios/Challenges/teleport.yaml view
@@ -58,6 +58,7 @@ world: default: [ice, water] palette:+ ',': [ice, water] ' ': [ice, water] '~': [ice, wavy water] '*': [grass, flower]@@ -74,9 +75,9 @@ upperleft: [-1, 3] map: | ~~ ~ ~ ~- ~┌─────┐~ ┌─────┐~ + ~┌─────┐~ ┌─────┐~ , ┌┘.....└───┘T...*└┐ ~- │_....._ _..λ.._│ + │_....._ _..λ.._│ , └┐.....┌───┐*...T┌┘ ~- ~└─────┘ ~~└─────┘~ + ~└─────┘ ~~└─────┘~ , ~~~ ~ ~ ~
+ data/scenarios/Challenges/wolf-goat-cabbage.yaml view
@@ -0,0 +1,125 @@+version: 1+name: Lake crossing+author: Karl Ostmo+description: |+ Deposit all three items on the eastern shore of the lake.+creative: false+robots:+ - name: base+ display:+ char: 'U'+ attr: wood+ dir: [0, 1]+ devices:+ - ADT calculator+ - branch predictor+ - treads+ - lodestone+ - compass+ - comparator+ - counter+ - dictionary+ - grabber+ - lambda+ - logger+ - mirror+ - net+ - scanner+ - strange loop+ - boat+ - name: east_detector+ system: true+ dir: [0, 1]+ display:+ invisible: true+ - name: west_detector+ system: true+ dir: [0, 1]+ display:+ invisible: true+objectives:+ - goal:+ - |+ Ferry all three of the wolf, goat, and cabbage across the lake.+ However, only one of these can be carried at a time.+ - |+ Furthermore, if left unattended together, the wolf will eat the goat,+ or the goat will eat the cabbage.+ condition: |+ run "data/scenarios/Challenges/_wolf-goat-cabbage/together-on-east-bank.sw";+ prerequisite:+ previewable: true+ logic:+ not:+ or: + - id: unattended_disaster+ - id: carrying_two_items+ - goal:+ - Carrying two items at once is not allowed!+ condition: |+ as base {+ run "data/scenarios/Challenges/_wolf-goat-cabbage/multi-item-possession.sw";+ };+ id: carrying_two_items+ optional: true+ hidden: true+ - id: unattended_disaster+ teaser: Something was eaten+ goal:+ - Leaving an animal alone with its food is not allowed!+ condition: |+ run "data/scenarios/Challenges/_wolf-goat-cabbage/unattended-together.sw";+ optional: true+ hidden: true +solution: |+ run "scenarios/Challenges/_wolf-goat-cabbage/cross.sw"+entities:+ - name: wolf+ display:+ char: 'w'+ attr: silver+ description:+ - A wolf. Likes to eat goats.+ properties: [known, portable]+ - name: goat+ display:+ char: 'g'+ attr: sand+ description:+ - A goat. Likes to eat cabbage.+ properties: [known, portable]+ - name: cabbage+ display:+ char: 'c'+ attr: green+ description:+ - A cabbage.+ properties: [known, portable]+known: [water, boulder]+seed: 0+world:+ default: [blank]+ palette:+ 'A': [stone, boulder]+ 'B': [stone, boulder, base]+ '.': [grass]+ 'e': [grass, null, east_detector]+ '~': [dirt, water]+ 'w': [grass, wolf]+ 'g': [grass, goat]+ 'c': [grass, cabbage, west_detector]+ upperleft: [0, 0]+ map: |-+ AAAAAAAAAAAAAAAAAAAAA+ AAAAAAAA~~~~~AAAAAAAA+ AAAAA~~~~~~~~~~~AAAAA+ AAA~~~~~~~~~~~~~~~AAA+ AA~~~~~~~~~~~~~~~~~AA+ Aw~~~~~~~~~~~~~~~~~.A+ Ag~~~~~~~~~~~~~~~~~.A+ Ac~~~~~~~~~~~~~~~~~eA+ AB~~~~~~~~~~~~~~~~~AA+ AAA~~~~~~~~~~~~~~~AAA+ AAAAA~~~~~~~~~~~AAAAA+ AAAAAAAA~~~~~AAAAAAAA+ AAAAAAAAAAAAAAAAAAAAA
+ data/scenarios/Challenges/word-search.yaml view
@@ -0,0 +1,387 @@+version: 1+name: Word search+author: Karl Ostmo+description: |+ Find the word+seed: 2+creative: false+objectives:+ - id: prepare_playfield+ teaser: Wait patiently+ goal:+ - |+ First, wait for the playfield to be set up,+ and then your path forward will be cleared.+ - |+ You can speed this process up with CTRL+x.+ condition: |+ as base {knows "capital C"};+ - goal:+ - Use the `drill` command (e.g. "drill down" when on top of the+ intended letter) to mark the sequence of letters that+ spells COW within the designated playfield.+ - |+ The sequence may appear horizontally in either the+ leftward or rightward direction,+ or vertically in either the upward or downward direction.+ Diagonal appearances are not valid.+ prerequisite: prepare_playfield+ condition: |+ /**+ Algorithm:+ We only need to check the base's+ current position: if we find three contiguous highlights,+ then we know that the player has just completed their+ third highlight.+ */++ def whichOrdinal =+ isC <- ishere "lowercase c";+ if (isC) {+ return 0;+ } {+ isO <- ishere "lowercase o";+ if (isO) {+ return 1;+ } {+ isW <- ishere "lowercase w";+ if (isW) {+ return 2;+ } {+ return (-1);+ }+ }+ }+ end;+++ def whichHighlightedOrdinal = \str.+ if (str == "lowercase c") {+ return 0;+ } {+ if (str == "lowercase o") {+ return 1;+ } {+ if (str == "lowercase w") {+ return 2;+ } {+ return (-1);+ }+ }+ }+ end;++ def countConsecutive = \expectedOrdinal. \n.++ thisOrdinal <- whichOrdinal;+ nextOrdinal <- if (thisOrdinal == expectedOrdinal) {+ return $ expectedOrdinal + 1;+ } {+ return 0;+ };++ if (nextOrdinal == 3) {+ return true;+ } {+ if (n > 1) {+ move;+ countConsecutive nextOrdinal $ n - 1;+ } {+ return false;+ };+ };+ end;++ def checkBackAndForth =++ foundBackward <- countConsecutive 0 3;+ if (foundBackward) {+ return true;+ } {+ turn back;+ countConsecutive 0 3;+ }+ end;++ def checkDirections = \n.+ if (n > 0) {+ wasFound <- checkBackAndForth;+ if wasFound {+ return true;+ } {+ turn left;+ checkDirections $ n - 1;+ }+ } {+ return false;+ }+ end;++ def isMarkedInDirection = \d.+ scanResult <- scan d;+ ordinalNum <- case scanResult+ (\_. return (-1))+ whichHighlightedOrdinal;+ return $ ordinalNum >= 0;+ end;++ /**+ It's possible we could be one cell away from+ a marked cell after finishing, either due+ to using a directional `drill` command instead of+ `drill down`, or due to an apparent bug which+ does not evaluate the goal condition between the+ `drill` and a `move` command.+ */+ def moveToMarkedCell = \n.+ if (n > 0) {+ isMarkedAhead <- isMarkedInDirection forward;+ if isMarkedAhead {+ move;+ return true;+ } {+ turn left;+ moveToMarkedCell $ n - 1;+ };+ } {+ return false;+ };+ end;++ /**+ Orient ourselves such that+ a marked cell is behind us.+ */+ def findMarkBehind = \n.+ if (n > 0) {+ isMarkedBehind <- isMarkedInDirection back;+ if isMarkedBehind {+ return true;+ } {+ turn left;+ findMarkBehind $ n - 1;+ };+ } {+ return false;+ };+ end;++ /**+ The cell we're on might be in the middle of a word,+ rather than the end. Determine the orientation of+ the line, then move along it until reaching the end.++ Algorithm:+ 0. Assumption: we are currently on a marked cell.+ 1. Turn in all all four directions to `scan back`+ for a second marked cell. Stop turning if+ we encounter one.+ If none found after 4 turns, abort.+ 2. `scan forward` to see if there is a marked cell in+ the opposite direction.+ `move` (foward) once if there is. Since the word+ is only three cells long, this will be the other+ end of it.+ */+ def moveToWordExtrema =+ foundCellBehind <- findMarkBehind 4;+ if foundCellBehind {+ isMarkedAhead <- isMarkedInDirection forward;+ if isMarkedAhead {+ move;+ } {};+ } {};+ end;++ def checkSoln =+ isMarkedHere <- isMarkedInDirection down;+ atMarkedCell <- if isMarkedHere {+ return true;+ } {+ moveToMarkedCell 4;+ };++ if atMarkedCell {+ moveToWordExtrema;+ checkDirections 4;+ } {+ return false;+ }+ end;++ as base {checkSoln};++robots:+ - name: base+ display:+ attr: flower+ dir: [-1, 0]+ devices:+ - treads+ - ADT calculator+ - branch predictor+ - GPS receiver+ - clock+ - comparator+ - counter+ - dictionary+ - lambda+ - logger+ - mirror+ - net+ - scanner+ - strange loop+ - string+ - highlighter+ inventory:+ - [3, ink]+ - name: lettersetter+ system: true+ dir: [1, 0]+ display:+ invisible: true+ inventory:+ - [376, capital C]+ - [376, capital O]+ - [376, capital W]+ program: |+ run "scenarios/Challenges/_word-search/create-puzzle.sw"+solution: |+ // Hard-coded for seed=2:+ def doN = \n. \f. if (n > 0) {f; doN (n - 1) f} {}; end;+ def waitUntilUnblocked =+ x <- blocked;+ if x {+ wait 1;+ waitUntilUnblocked;+ } {};+ end;+ waitUntilUnblocked;+ doN 11 move;+ turn right;+ doN 2 move;+ drill down;+ move;+ drill down;+ move;+ drill down;+ // run "scenarios/Challenges/_word-search/solution.sw"+entities:+ - name: capital C+ display:+ char: 'C'+ description:+ - C is for California+ - name: capital O+ display:+ char: 'O'+ description:+ - O is for Oklahoma+ - name: capital W+ display:+ char: 'W'+ description:+ - W is for Washington+ - name: lowercase c+ display:+ char: 'c'+ attr: gold+ description:+ - c is for cantaloupe+ - name: lowercase o+ display:+ char: 'o'+ attr: gold+ description:+ - o is for orange+ - name: lowercase w+ display:+ char: 'w'+ attr: gold+ description:+ - w is for watermelon+ - name: highlighter+ display:+ char: 'P'+ attr: gold+ capabilities: [drill]+ description:+ - Instrument for marking found words+ properties: [known, portable]+ - name: ink+ display:+ char: 'K'+ attr: gold+ description:+ - Ink for marking found words+ properties: [known, portable]+recipes:+ - in:+ - [1, capital C]+ - [1, ink]+ out:+ - [1, lowercase c]+ required:+ - [1, highlighter]+ - in:+ - [1, capital O]+ - [1, ink]+ out:+ - [1, lowercase o]+ required:+ - [1, highlighter]+ - in:+ - [1, capital W]+ - [1, ink]+ out:+ - [1, lowercase w]+ required:+ - [1, highlighter]+ - in:+ - [1, lowercase c]+ out:+ - [1, capital C]+ - [1, ink]+ required:+ - [1, highlighter]+ - in:+ - [1, lowercase o]+ out:+ - [1, capital O]+ - [1, ink]+ required:+ - [1, highlighter]+ - in:+ - [1, lowercase w]+ out:+ - [1, capital W]+ - [1, ink]+ required:+ - [1, highlighter]+known: [boulder]+world:+ default: [dirt]+ upperleft: [0, 0]+ offset: false+ palette:+ '@': [stone, boulder]+ '.': [grass]+ 'a': [grass, null, lettersetter]+ '#': [dirt]+ Ω: [stone, null, base]+ map: |+ a........................###+ .........................###+ .........................###+ .........................###+ .........................###+ .........................###+ .........................###+ .........................###+ .........................###+ .........................@@@+ .........................@Ω@+ .........................@@@+ .........................###+ .........................###+ .........................###
data/scenarios/Fun/00-ORDER.txt view
@@ -1,1 +1,2 @@ GoL.yaml+logo-burst.yaml
+ data/scenarios/Fun/_logo-burst/coordinator.sw view
@@ -0,0 +1,12 @@+def forever : cmd unit -> cmd unit = \c. c ; forever c end++def alternate =+ wait 50;+ say "Green light";+ make "bit (1)";+ wait 100;+ say "Red light";+ make "bit (0)";+ end;++forever alternate;
+ data/scenarios/Fun/_logo-burst/drone.sw view
@@ -0,0 +1,67 @@++def repeat : int -> cmd unit -> cmd unit =+ \n. \c. if (n == 0) {} {c ; repeat (n-1) c}+end+def abs = \n. if (n < 0) {-n} {n} end+def elif = \t. \then. \else. {if t then else} end+def else = \t. t end++def randdir : cmd dir =+ d <- random 4;+ return (+ if (d == 0) {north}+ $ elif (d == 1) {east}+ $ elif (d == 2) {south}+ $ else {west}+ )+end;++def wander =+ d <- randdir;+ turn d;+ dist <- random 4;+ try {repeat dist move} {};+end;++def disperse =+ r <- robotnamed "stoplight";+ greenLight <- as r {has "bit (1)"};+ if greenLight {+ wander;+ disperse;+ } {};+ end;++def converge = \loc. \currentLoc.++ let xDist = fst currentLoc - fst loc in+ let yDist = snd currentLoc - snd loc in++ if (xDist < 0) {+ turn east;+ } {+ if (xDist > 0) {+ turn west;+ } {};+ };+ repeat (abs xDist) move;++ if (yDist < 0) {+ turn north;+ } {+ if (yDist > 0) {+ turn south;+ } {};+ };+ repeat (abs yDist) move;+ end;++def go = \loc.+ disperse;+ currentLoc <- whereami;+ converge loc currentLoc;+ go loc;+ end;++loc <- whereami;+go loc;
+ data/scenarios/Fun/logo-burst.yaml view
@@ -0,0 +1,84 @@+version: 1+name: Logo burst+author: Karl Ostmo+description: |+ Animate the Swarm logo+creative: false+seed: 0+robots:+ # We include a base robot to stabilize the screen at a fixed position.+ - name: base+ display:+ invisible: true+ dir: [1, 0]+ devices:+ - logger+ - hearing aid+ - name: botRight+ system: true+ display:+ invisible: false+ dir: [1, 0]+ program: |+ run "scenarios/Fun/_logo-burst/drone.sw"+ - name: botUp+ system: true+ display:+ invisible: false+ dir: [0, 1]+ program: |+ run "scenarios/Fun/_logo-burst/drone.sw"+ - name: botLeft+ system: true+ display:+ invisible: false+ dir: [-1, 0]+ program: |+ run "scenarios/Fun/_logo-burst/drone.sw"+ - name: botDown+ system: true+ display:+ invisible: false+ dir: [0, -1]+ program: |+ run "scenarios/Fun/_logo-burst/drone.sw"+ - name: stoplight+ system: true+ display:+ invisible: false+ dir: [0, 1]+ devices:+ - lodestone+ inventory:+ - [1, bit (0)]+ program: |+ run "scenarios/Fun/_logo-burst/coordinator.sw"+ +known: [boulder, tree, water, wavy water]+world:+ default: [blank]+ upperleft: [0, 0]+ offset: false+ palette:+ 'Ω': [blank, null, base]+ 's': [blank, null, stoplight]+ '.': [blank]+ ' ': [blank]+ '@': [blank, boulder]+ 'T': [blank, tree]+ '▒': [blank, water]+ '~': [blank, wavy water]+ '>': [blank, null, botRight]+ '^': [blank, null, botUp]+ '<': [blank, null, botLeft]+ 'v': [blank, null, botDown]+ map: |+ s v .+ . . + . v<^vv<<@ ^^ vv >^v^T ^^^v< <>< v >+ T ^< >^ < >> v> v< >T <v> <<▒^ @><> .+ . <@v^^>> @v > <T <v<T<Ω^> v><v< << <<T^ v< . + . > >> v@ @@> << ~v < >~ ^~ >> @^ >v .+ . >>^v^^^ < ~ T~v v< <~ >>T <v vv ▒> . + . ▒ . + . ^ ^ v > > .
data/scenarios/Speedruns/curry.yaml view
@@ -1,10 +1,12 @@ version: 1 name: Curry author: Brent Yorgey-description: Race to make a bowl of curry as fast as possible.+description: Race to make a bowl of curry as quickly as possible. See the Swarm wiki for more information on Swarm speedrunning. objectives:- - condition: as base {has "curry"}+ - goal:+ - Make a bowl of curry as quickly as possible!+ condition: as base {has "curry"} robots: - name: base loc: [0,0]
data/scenarios/Speedruns/forester.yaml view
@@ -4,7 +4,9 @@ description: Race to harvest 1024 trees as quickly as possible. See the Swarm wiki for more information on Swarm speedrunning. objectives:- - condition: as base {n <- count "tree"; return (n >= 1024)}+ - goal:+ - Harvest 1024 trees as quickly as possible!+ condition: as base {n <- count "tree"; return (n >= 1024)} robots: - name: base loc: [0,0]
data/scenarios/Speedruns/mithril.yaml view
@@ -4,7 +4,9 @@ description: Race to mine some mithril. See the Swarm wiki for more information on Swarm speedrunning. objectives:- - condition: as base {has "mithril"}+ - goal:+ - Mine some mithril as quickly as possible!+ condition: as base {has "mithril"} robots: - name: base loc: [0,0]
data/scenarios/Testing/00-ORDER.txt view
@@ -13,4 +13,13 @@ 378-objectives.yaml 684-swap.yaml 699-movement-fail+858-inventory+795-prerequisite 710-multi-robot.yaml+920-meet.yaml+955-heading.yaml+397-wrong-missing.yaml+961-custom-capabilities.yaml+956-GPS.yaml+958-isempty.yaml+1034-custom-attributes.yaml
+ data/scenarios/Testing/1034-custom-attributes.yaml view
@@ -0,0 +1,134 @@+version: 1+name: Rainbow color custom attributes+description: |+ Custom attributes with rainbow colors+creative: false+attrs:+ - name: rainbow1+ fg: "#ffadad"+ - name: rainbow2+ fg: "#ffd6a5"+ - name: rainbow3+ fg: "#ffff96"+ - name: rainbow4+ fg: "#caffbf"+ - name: rainbow5+ fg: "#9bf6ff"+ - name: rainbow6+ fg: "#a0c4ff"+ - name: rainbow7+ fg: "#bdb2ff"+ - name: redOnYellow+ fg: "#ff0000"+ bg: ffff00+ - name: cyanOnMagenta+ fg: 00ffff+ bg: "#ff00ff"+ - name: italicAndUnderline+ style:+ - Italic+ - Underline+ - name: boldAndStrikethrough+ style:+ - Bold+ - Strikethrough+entities:+ - name: color1+ display:+ char: '▒'+ attr: rainbow1+ description:+ - c1+ properties: [known]+ - name: color2+ display:+ char: '▒'+ attr: rainbow2+ description:+ - c2+ properties: [known]+ - name: color3+ display:+ char: '▒'+ attr: rainbow3+ description:+ - c3+ properties: [known]+ - name: color4+ display:+ char: '▒'+ attr: rainbow4+ description:+ - c4+ properties: [known]+ - name: color5+ display:+ char: '▒'+ attr: rainbow5+ description:+ - c5+ properties: [known]+ - name: color6+ display:+ char: '▒'+ attr: rainbow6+ description:+ - c6+ properties: [known]+ - name: color7+ display:+ char: '▒'+ attr: rainbow7+ description:+ - c7+ properties: [known]+ - name: redYellow+ display:+ char: 'R'+ attr: redOnYellow+ description:+ - Red on Yellow+ properties: [known]+ - name: cyanMagenta+ display:+ char: 'C'+ attr: cyanOnMagenta+ description:+ - Cyan on Magenta+ properties: [known]+ - name: italicUnderline+ display:+ char: 'X'+ attr: italicAndUnderline+ description:+ - Italic and Underline+ properties: [known]+ - name: boldStrikethrough+ display:+ char: 'X'+ attr: boldAndStrikethrough+ description:+ - Bold and Strikethrough+ properties: [known]+robots: []+world:+ default: [blank]+ palette:+ '.': [blank]+ '1': [blank, color1]+ '2': [blank, color2]+ '3': [blank, color3]+ '4': [blank, color4]+ '5': [blank, color5]+ '6': [blank, color6]+ '7': [blank, color7]+ 'R': [blank, redYellow]+ 'C': [blank, cyanMagenta]+ 'I': [blank, italicUnderline]+ 'B': [blank, boldStrikethrough]+ upperleft: [0, 0]+ map: |-+ .1234567..R.....II.+ .1234567..R........+ .1234567.....C..BB.+ .1234567.....C.....
data/scenarios/Testing/201-require/201-require-device-creative.yaml view
@@ -8,10 +8,10 @@ try { r <- robotNumbered 1; p <- as r {whereami};- boatInstalled <- as r {installed "boat"};+ boatEquipped <- as r {equipped "boat"}; b1 <- as r {count "boat"}; b0 <- as base {count "boat"};- return (p == (2,0) && b0 == 0 && boatInstalled && b1 == 0);+ return (p == (2,0) && b0 == 0 && boatEquipped && b1 == 0); } { return false } creative: true solution: |
data/scenarios/Testing/201-require/201-require-device-creative1.yaml view
@@ -8,10 +8,10 @@ try { r <- robotNumbered 1; p <- as r {whereami};- boatInstalled <- as r {installed "boat"};+ boatEquipped <- as r {equipped "boat"}; b1 <- as r {count "boat"}; b0 <- as base {count "boat"};- return (p == (2,0) && b0 == 1 && boatInstalled && b1 == 0);+ return (p == (2,0) && b0 == 1 && boatEquipped && b1 == 0); } { return false } creative: true solution: |
data/scenarios/Testing/201-require/201-require-device.yaml view
@@ -8,10 +8,10 @@ try { r <- robotNumbered 1; p <- as r {whereami};- boatInstalled <- as r {installed "boat"};+ boatEquipped <- as r {equipped "boat"}; b1 <- as r {count "boat"}; b0 <- as base {count "boat"};- return (p == (2,0) && b0 == 0 && boatInstalled && b1 == 0);+ return (p == (2,0) && b0 == 0 && boatEquipped && b1 == 0); } { return false } solution: | build {require "boat"; move; require "boat"; move}
data/scenarios/Testing/201-require/533-reprogram-simple.yaml view
@@ -1,7 +1,7 @@ version: 1-name: Install devices while reprogramming+name: Equip devices while reprogramming description: |- While executing 'reprogram', we should install any required devices which+ While executing 'reprogram', we should equip any required devices which the target robot doesn't have. https://github.com/swarm-game/swarm/pulls/533 objectives:@@ -14,13 +14,13 @@ fred <- robotNamed "fred"; p <- as fred {whereami};- boatInstalled <- as fred {installed "boat"};- drillInstalled <- as fred {installed "metal drill"};- solarInstalled <- as fred {installed "solar panel"};- treadsInstalled <- as fred {installed "treads"};+ boatEquipped <- as fred {equipped "boat"};+ drillEquipped <- as fred {equipped "metal drill"};+ solarEquipped <- as fred {equipped "solar panel"};+ treadsEquipped <- as fred {equipped "treads"}; return (p == (2,0)- && boatInstalled && drillInstalled && solarInstalled && treadsInstalled+ && boatEquipped && drillEquipped && solarEquipped && treadsEquipped && base_boats == 1 && base_solar == 1 && base_treads == 1 && base_drills == 1 );
data/scenarios/Testing/201-require/533-reprogram.yaml view
@@ -1,7 +1,7 @@ version: 1-name: Install devices + entities while reprogramming+name: Equip devices + entities while reprogramming description: |- While executing 'reprogram', we should install any required devices and+ While executing 'reprogram', we should equip any required devices and give required entities which the target robot doesn't have. https://github.com/swarm-game/swarm/pulls/533 objectives:@@ -15,11 +15,11 @@ fred <- robotNamed "fred"; p <- as fred {whereami};- boatInstalled <- as fred {installed "boat"};- drillInstalled <- as fred {installed "metal drill"};+ boatEquipped <- as fred {equipped "boat"};+ drillEquipped <- as fred {equipped "metal drill"}; fred_rocks <- as fred {count "rock"}; - return (p == (2,0) && boatInstalled && drillInstalled+ return (p == (2,0) && boatEquipped && drillEquipped && base_boats == 1 && base_solar == 1 && base_treads == 1 && base_drills == 1 && base_rocks == 42 && fred_rocks == 8
data/scenarios/Testing/378-objectives.yaml view
@@ -3,7 +3,9 @@ description: | First, get some trees; then, use them to build a harvester. objectives:- - goal:+ - id: get_trees+ teaser: Get trees+ goal: - Your first goal is to get three trees. condition: | try {@@ -14,6 +16,7 @@ - Nice job. Now, build a harvester. condition: | try { as base {has "harvester"} } {return false}+ prerequisite: get_trees solution: | build {turn right; move; move; grab; move; grab; move; grab; turn back; move; move; move; move}; wait 16;
data/scenarios/Testing/394-build-drill.yaml view
@@ -9,7 +9,7 @@ try { as base {l <- has "detonator"; return (not l)} } { return false }-# When testing, add `s <- build {...}; n <- as s {whoami}; log n` +# When testing, add `s <- build {...}; n <- as s {whoami}; log n` solution: | def forever = \c. c ; forever c end; def unblock = try {drill forward} {} end;@@ -18,7 +18,13 @@ r <- build { wait 2; log "Hi, I am builder";- forever (build {log "Hi, I am pusher"; turn forward; forever push}; log "- robot built")+ forever (+ build {+ log "Hi, I am pusher";+ forever push+ };+ log "- robot built"+ ) }; wait 10; place "detonator";
+ data/scenarios/Testing/397-wrong-missing.yaml view
@@ -0,0 +1,38 @@+version: 1+name: Test issue 397 (wrong device reported missing)+description: |+ A scenario designed to trigger issue 397, where in certain+ situations involving a capability with no candidate devices,+ an incorrect error message was generated reporting some other+ device to be missing.+ https://github.com/swarm-game/swarm/issues/397+objectives:+ - condition: |+ t <- time; return (t == 2)+ goal:+ - |+ This is a dummy condition that just ensures the base has had+ time to run the problematic `build` command. The scenario+ *should* generate an error message; what we really care about is+ whether the generated error message is correct, which is checked+ in test/integration/Main.hs .+solution: |+ build {move; turn right; loc <- whereami}+robots:+ - name: base+ dir: [0,1]+ devices:+ - 3D printer+ - logger+ inventory:+ - [1, treads]+ - [1, solar panel]+world:+ default: [blank]+ palette:+ 'Ω': [grass, null, base]+ '.': [grass]+ upperleft: [0,1]+ map: |+ .+ Ω
data/scenarios/Testing/562-lodestone.sw view
@@ -32,10 +32,8 @@ until (ishere "lodestone") {grab}; // get two bit (0)-// TODO: require should not be necessary build { log "Hi!";- require "branch predictor"; repeat ( log "I am going for a bit"; turn east; m2; x <- until (ishere "bit (0)") {harvest}; turn back; m2; place x;
data/scenarios/Testing/562-lodestone.yaml view
@@ -27,6 +27,7 @@ - branch predictor inventory: - [10, drill]+ - [10, lambda] - [10, logger] - [10, compass] - [10, treads]
+ data/scenarios/Testing/795-prerequisite/00-ORDER.txt view
@@ -0,0 +1,4 @@+795-prerequisite-or.yaml+795-prerequisite-and.yaml+795-prerequisite-mutually-exclusive.yaml+795-prerequisite-cycle-with-not.yaml
+ data/scenarios/Testing/795-prerequisite/795-prerequisite-and.yaml view
@@ -0,0 +1,50 @@+version: 1+name: |+ Prerequisite objectives: AND+description: |+ Complete an objective with a prerequisite entailing both of two other objectives.+objectives:+ - goal:+ - Achieve both of two other objectives+ condition: |+ return true;+ prerequisite:+ previewable: true+ logic:+ and:+ - have_furnace+ - have_gear+ - id: have_furnace+ goal:+ - Make a "furnace".+ condition: |+ as base {has "furnace"};+ optional: true+ - id: have_gear+ goal:+ - Make a "wooden gear".+ condition: |+ as base {has "wooden gear"};+ optional: true+solution: |+ make "furnace";+ make "wooden gear";+robots:+ - name: base+ display:+ char: 'Ω'+ attr: robot+ dir: [0, 1]+ devices:+ - workbench+ - grabber+ inventory:+ - [2, board]+ - [5, rock]+world:+ default: [blank]+ palette:+ 'x': [grass, null, base]+ upperleft: [0, 0]+ map: |+ x
+ data/scenarios/Testing/795-prerequisite/795-prerequisite-cycle-with-not.yaml view
@@ -0,0 +1,42 @@+version: 1+name: |+ Prerequisite objectives: non-dependency cycle due to the NOT+description: |+ This should NOT be rejected by the parser.+ The two objectives do reference each other,+ but the NOT is a distinct reference from the non-negated goal.+objectives:+ - id: have_furnace+ goal:+ - Make a "furnace".+ condition: |+ as base {has "furnace"};+ prerequisite:+ not: have_gear+ - id: have_gear+ goal:+ - Make a "wooden gear".+ condition: |+ as base {has "wooden gear"};+ prerequisite: have_furnace+solution: |+ make "wooden gear";+ make "furnace";+robots:+ - name: base+ display:+ char: 'Ω'+ attr: robot+ dir: [0, 1]+ devices:+ - workbench+ inventory:+ - [2, board]+ - [5, rock]+world:+ default: [blank]+ palette:+ 'x': [grass, null, base]+ upperleft: [0, 0]+ map: |+ x
+ data/scenarios/Testing/795-prerequisite/795-prerequisite-mutually-exclusive.yaml view
@@ -0,0 +1,61 @@+version: 1+name: |+ Mutually exclusive prerequisites+description: |+ Complete an objective with a prerequisite of either of two other objectives.+objectives:+ - goal:+ - Either obtain a "furnace" WITHOUT a "flower", or obtain both a "wooden gear" and "flower".+ condition: |+ return true;+ prerequisite:+ previewable: true+ logic:+ or:+ - and:+ - id: have_furnace+ - not:+ id: have_flower+ - and:+ - id: have_gear+ - id: have_flower+ - id: have_furnace+ goal:+ - Make a "furnace".+ condition: |+ as base {has "furnace"};+ + optional: true+ - id: have_gear+ goal:+ - Make a "wooden gear".+ condition: |+ as base {has "wooden gear"};+ optional: true+ - id: have_flower+ goal:+ - Possess a "flower".+ condition: |+ as base {has "flower"};+ optional: true+solution: |+ make "furnace"+robots:+ - name: base+ display:+ char: 'Ω'+ attr: robot+ dir: [0, 1]+ devices:+ - workbench+ - grabber+ inventory:+ - [2, board]+ - [5, rock]+world:+ default: [blank]+ palette:+ 'x': [grass, flower, base]+ upperleft: [0, 0]+ map: |+ x
+ data/scenarios/Testing/795-prerequisite/795-prerequisite-or.yaml view
@@ -0,0 +1,49 @@+version: 1+name: |+ Prerequisite objectives: OR+description: |+ Complete an objective with a prerequisite of either of two other objectives.+objectives:+ - goal:+ - Achieve one of two other objectives+ condition: |+ return true;+ prerequisite:+ previewable: true+ logic:+ or:+ - id: have_furnace+ - id: have_gear+ - id: have_furnace+ goal:+ - Make a "furnace".+ condition: |+ as base {has "furnace"};+ optional: true+ - id: have_gear+ goal:+ - Make a "wooden gear".+ condition: |+ as base {has "wooden gear"};+ optional: true+solution: |+ make "wooden gear"+robots:+ - name: base+ display:+ char: 'Ω'+ attr: robot+ dir: [0, 1]+ devices:+ - workbench+ - grabber+ inventory:+ - [2, board]+ - [5, rock]+world:+ default: [blank]+ palette:+ 'x': [grass, flower, base]+ upperleft: [0, 0]+ map: |+ x
+ data/scenarios/Testing/858-inventory/00-ORDER.txt view
@@ -0,0 +1,3 @@+858-possession-objective.yaml+858-nonpossession-objective.yaml+858-counting-objective.yaml
+ data/scenarios/Testing/858-inventory/858-counting-objective.yaml view
@@ -0,0 +1,36 @@+version: 1+name: Evaluate count of an item+description: |+ The goal criteria is exactly the same as+ 858-possession-objective but evaluated+ with the "count" function instead.+creative: false+objectives:+ - goal:+ - |+ Get 1 tree.+ condition: |-+ as base {+ itemCount <- count "tree";+ return $ itemCount > 0;+ };+robots:+ - name: base+ dir: [0, 1]+ devices:+ - treads+ - scanner+ - grabber+solution: |+ move;+ grab;+known: [tree]+world:+ default: [blank]+ palette:+ 'B': [grass, null, base]+ 'w': [grass, tree]+ upperleft: [0, 0]+ map: |-+ w+ B
+ data/scenarios/Testing/858-inventory/858-nonpossession-objective.yaml view
@@ -0,0 +1,35 @@+version: 1+name: Evaluate nonpossession of an item+description: |+ Opposite test case of 858-possession-objective+creative: false+objectives:+ - goal:+ - |+ Get rid of 1 tree.+ condition: |-+ as base {+ x <- has "tree";+ return $ not x;+ };+robots:+ - name: base+ dir: [0, 1]+ devices:+ - treads+ - scanner+ - grabber+ inventory:+ - [1, tree]+solution: |+ place "tree";+known: [tree]+world:+ default: [blank]+ palette:+ 'B': [grass, null, base]+ 'w': [grass]+ upperleft: [0, 0]+ map: |-+ w+ B
+ data/scenarios/Testing/858-inventory/858-possession-objective.yaml view
@@ -0,0 +1,33 @@+version: 1+name: Evaluate possession of an item+description: |+ test case+creative: false+objectives:+ - goal:+ - |+ Get 1 tree.+ condition: |-+ as base {+ has "tree";+ };+robots:+ - name: base+ dir: [0, 1]+ devices:+ - treads+ - scanner+ - grabber+solution: |+ move;+ grab;+known: [tree]+world:+ default: [blank]+ palette:+ 'B': [grass, null, base]+ 'w': [grass, tree]+ upperleft: [0, 0]+ map: |-+ w+ B
+ data/scenarios/Testing/920-meet.yaml view
@@ -0,0 +1,52 @@+version: 1+name: Test meet and meetAll commands+description: |+ Make sure meet prefers a robot on the same cell, and test meetAll+ by giving everyone a boat.+objectives:+ - condition: |+ r0 <- robotNamed "other0";+ b0 <- as r0 { has "boat" };+ teleport self (0,0);+ b1 <- meetAll (\b. \r. b0 <- as r {has "boat"}; return (b && b0)) true;+ n2 <- as r0 { count "boat" };+ return (b0 && b1 && (n2 == 2))+solution: |+ mr0 <- meet;+ case mr0 (\_. return ()) (\r0. give r0 "boat");+ meetAll (\_. \r. give r "boat") ()+robots:+ - name: base+ loc: [0,0]+ dir: [1,0]+ devices:+ - logger+ - antenna+ - ADT calculator+ - grabber+ inventory:+ - [7, boat]+ - name: other0+ loc: [0,0]+ dir: [1,0]+ - name: other+ dir: [1,0]+world:+ default: [blank]+ palette:+ '.': [grass]+ 'Ω': [grass, null]+ 'o': [grass, null, other]+ '┌': [stone, upper left corner]+ '┐': [stone, upper right corner]+ '└': [stone, lower left corner]+ '┘': [stone, lower right corner]+ '─': [stone, horizontal wall]+ '│': [stone, vertical wall]+ upperleft: [-2, 2]+ map: |+ ┌───┐+ │.o.│+ │oΩo│+ │.o.│+ └───┘
+ data/scenarios/Testing/955-heading.yaml view
@@ -0,0 +1,28 @@+version: 1+name: Test heading command+description: |+ Test the `heading` command.+ https://github.com/swarm-game/swarm/issues/955+objectives:+ - condition: |+ loc <- as base {whereami};+ h <- as base {heading};+ return $ loc == (1,-1) && h == north+solution: |+ h <- heading; turn east; move; move; turn right; move; move; turn h+robots:+ - name: base+ dir: [0,1]+ devices:+ - treads+ - compass+world:+ default: [blank]+ palette:+ '^': [grass, null, base]+ '.': [grass]+ upperleft: [-1, 1]+ map: |+ ^..+ ...+ ...
+ data/scenarios/Testing/956-GPS.yaml view
@@ -0,0 +1,66 @@+version: 1+name: Test GPS receiver device+description: |+ Test the `whereami` command and GPS receiver device.+ https://github.com/swarm-game/swarm/issues/956+objectives:+ - condition: |+ loc <- as base {whereami};+ return $ loc == (0,0)+ goal:+ - |+ The goal is to move back to the origin after being+ unceremoniously teleported to a random location. The only way+ to do this is to use a GPS receiver device to learn the+ current location and use the result to decide how to move.+solution: |+ def x = \n. \c. if (n == 0) {} {c ; x (n-1) c} end;+ def abs = \n. if (n<0) {-n} {n} end;+ wait 2;+ loc <- whereami;+ turn (if (fst loc < 0) {east} {west}); x (abs (fst loc)) move;+ turn (if (snd loc < 0) {north} {south}); x (abs (snd loc)) move;+robots:+ - name: base+ dir: [0,1]+ loc: [-5,5]+ devices:+ - clock+ - dictionary+ - GPS receiver+ - treads+ - compass+ - ADT calculator+ - strange loop+ - comparator+ - calculator+ - branch predictor+ - logger+ - name: teleporter+ dir: [0,1]+ loc: [0,0]+ system: true+ display:+ invisible: true+ program: |+ x <- random 11;+ y <- random 11;+ teleport base (x-5, y-5)+world:+ default: [blank]+ palette:+ '.': [grass]+ upperleft: [-5, 5]+ map: |+ ...........+ ...........+ ...........+ ...........+ ...........+ ...........+ ...........+ ...........+ ...........+ ...........+ ...........+
+ data/scenarios/Testing/958-isempty.yaml view
@@ -0,0 +1,57 @@+version: 1+name: Test isempty+description: |+ Test the `isempty` command.+objectives:+ - condition: |+ as base {has "thneed"}+ goal:+ - |+ The goal is to pick up the thing right after the empty+ spot in the row.+solution: |+ def find_thing =+ e <- isempty;+ move;+ if e {} {find_thing}+ end;+ find_thing;+ grab+robots:+ - name: base+ dir: [1,0]+ devices:+ - scanner+ - branch predictor+ - grabber+ - strange loop+ - lambda+ - dictionary+ - logger+ - treads+ - solar panel+entities:+ - name: thneed+ display:+ attr: gold+ char: 'Y'+ description:+ - A thing that everyone needs!+ properties: [portable]+world:+ default: [blank]+ palette:+ '.': [grass]+ '>': [grass, tree, base]+ 'o': [grass, rock]+ '$': [grass, LaTeX]+ 'i': [grass, cotton]+ '0': [grass, bit (0)]+ '1': [grass, bit (1)]+ 'R': [grass, pixel (R)]+ 'G': [grass, pixel (G)]+ 'B': [grass, pixel (B)]+ 'Y': [grass, thneed]+ upperleft: [-5, 5]+ map: |+ >o$i01R.YGB
+ data/scenarios/Testing/961-custom-capabilities.yaml view
@@ -0,0 +1,42 @@+version: 1+name: Test issue 961 (custom entities providing capabilities)+description: |+ Test that custom entities providing a capability are considered+ when equipping a new robot.+ https://github.com/swarm-game/swarm/issues/961+objectives:+ - condition: |+ mover <- robotNumbered 1;+ as mover { loc <- whereami; return $ loc == (0,1) }+ goal:+ - |+ Get a robot to (0,1)!+solution: |+ build {move}+entities:+ - name: wheels+ display:+ attr: device+ char: 'o'+ description:+ - A non-traditional means of locomotion.+ properties: [known, portable]+ capabilities: [move]+robots:+ - name: base+ dir: [0,1]+ devices:+ - 3D printer+ - logger+ inventory:+ - [1, wheels]+ - [1, solar panel]+world:+ default: [blank]+ palette:+ 'Ω': [grass, null, base]+ '.': [grass]+ upperleft: [0,1]+ map: |+ .+ Ω
+ data/scenarios/Testing/_Validation/795-prerequisite-cycle.yaml view
@@ -0,0 +1,38 @@+version: 1+name: |+ Prerequisite objectives: dependency cycle+description: |+ This should be rejected by the parser.+objectives:+ - id: have_furnace+ goal:+ - Make a "furnace".+ condition: |+ as base {has "furnace"};+ prerequisite: have_gear+ - id: have_gear+ goal:+ - Make a "wooden gear".+ condition: |+ as base {has "wooden gear"};+ prerequisite: have_furnace+solution: |+ make "wooden gear"+robots:+ - name: base+ display:+ char: 'Ω'+ attr: robot+ dir: [0, 1]+ devices:+ - workbench+ inventory:+ - [2, board]+ - [5, rock]+world:+ default: [blank]+ palette:+ 'x': [grass, null, base]+ upperleft: [0, 0]+ map: |+ x
+ data/scenarios/Testing/_Validation/795-prerequisite-nonexistent-reference.yaml view
@@ -0,0 +1,38 @@+version: 1+name: |+ Prerequisite objectives: Nonexistent reference+description: |+ This scenario should be unparseable due to typo "shave_furnace" vs "have_furnace"+objectives:+ - goal:+ - Achieve one of two other objectives+ condition: |+ return true;+ prerequisite: shave_furnace+ - id: have_furnace+ goal:+ - Make a "furnace".+ condition: |+ as base {has "furnace"};+ optional: true+solution: |+ make "furnace"+robots:+ - name: base+ display:+ char: 'Ω'+ attr: robot+ dir: [0, 1]+ devices:+ - workbench+ - grabber+ inventory:+ - [2, board]+ - [5, rock]+world:+ default: [blank]+ palette:+ 'x': [grass, flower, base]+ upperleft: [0, 0]+ map: |+ x
+ data/scenarios/Testing/_Validation/795-prerequisite-self-reference.yaml view
@@ -0,0 +1,31 @@+version: 1+name: |+ Prerequisite objectives: dependency cycle (self-reference)+description: |+ This should be rejected by the parser.+objectives:+ - id: have_furnace+ goal:+ - Make a "furnace".+ condition: |+ as base {has "furnace"};+ prerequisite: have_furnace+solution: |+ make "furnace"+robots:+ - name: base+ display:+ char: 'Ω'+ attr: robot+ dir: [0, 1]+ devices:+ - workbench+ inventory:+ - [5, rock]+world:+ default: [blank]+ palette:+ 'x': [grass, null, base]+ upperleft: [0, 0]+ map: |+ x
data/scenarios/Tutorials/00-ORDER.txt view
@@ -5,9 +5,9 @@ place.yaml types.yaml type-errors.yaml-bind.yaml-install.yaml+equip.yaml build.yaml+bind2.yaml crash.yaml scan.yaml def.yaml
− data/scenarios/Tutorials/bind.yaml
@@ -1,81 +0,0 @@-version: 1-name: Bind notation-description: |- Learn about command return types and how to bind the results.-objectives:- - goal:- - |- Every command returns a value. However, some simple commands, like- `move`, do not have any meaningful- value to return. Swarm has a special type, `unit`, with only one value,- called `()`. Since there is only one possible value of type- `unit`, returning it does not convey any information.- Thus, the type of `move` is `cmd unit`.- - |- Other commands do return a nontrivial value after executing.- For example, `grab` has type- `cmd text`, and returns the name of the- grabbed entity as a text value. Try it:- - |- move; grab- - |- To use the result of a command later, you need bind notation, which- consists of a variable name and a leftwards-pointing arrow- before the command. For example:- - |- move; t <- grab; place t- - |- In the above example, the result returned by `grab` is assigned- to the variable name `t`, which can then be used later.- This could be useful, for example, if you do not care what you- grabbed and just want to move it to another cell, or if you- are not sure of the name of the thing being grabbed.- - Once you are done experimenting, do `place "Win"` to finish this challenge.- (Note, you might need to `grab` the entity you are standing on- or move to an empty cell first, since each cell can only- contain at most one entity.)- condition: |- try {- w <- as base {has "Win"};- return (not w);- } { return false }-solution: |- place "Win"-entities:- - name: Win- display:- attr: device- char: 'W'- description:- - Do `place "Win"` once you are done with this challenge.- - You might need to `grab` the entity you are standing on- or move to an empty cell first.- properties: [known, portable]-robots:- - name: base- dir: [1,0]- devices:- - treads- - grabber- - logger- - compass- - dictionary- inventory:- - [1, Win]- - [0, tree]-world:- default: [blank]- palette:- '>': [grass, null, base]- 'T': [grass, tree]- '┌': [stone, upper left corner]- '┐': [stone, upper right corner]- '└': [stone, lower left corner]- '┘': [stone, lower right corner]- '─': [stone, horizontal wall]- '│': [stone, vertical wall]- upperleft: [-1, 1]- map: |- ┌───┐- │>TT│- └───┘
+ data/scenarios/Tutorials/bind2-solution.sw view
@@ -0,0 +1,11 @@+build {+ move; move;+ turn right;+ move; move; move; move; move; move;+ x <- grab;+ turn left;+ move;+ turn left;+ move; move;+ place x;+}
+ data/scenarios/Tutorials/bind2.yaml view
@@ -0,0 +1,111 @@+version: 1+name: Bind notation+creative: false+description: |+ Learn about command return types and how to bind the results.+objectives:+ - id: grab_artifact+ teaser: Retrieve artifact+ goal:+ - |+ A pedestal stands conspicuously empty in the center of the room.+ Perhaps its intended contents lie nearby?+ - |+ Build a robot to retrieve and restore the mystery artifact to its proper place!+ - |+ Note: If you find yourself stuck, you can select "Start over" from+ the "Quit" (CTRL+q) dialog.+ condition: |+ try {+ p <- robotnamed "floorspot";+ w <- as p {ishere "Hastur"};+ return (not w);+ } { return false }+ - goal:+ - |+ Your robot obtained the misplaced artifact! Next you need to put it back+ on the pedestal. But neither you nor your robot knows the+ artifact's name. How can it be placed?+ - |+ Every command returns a value. However, some simple commands, like+ `move`, do not have any meaningful+ value to return. Swarm has a special type, `unit`, with only one value,+ called `()`. Since there is only one possible value of type+ `unit`, returning it does not convey any information.+ Thus, the type of `move` is `cmd unit`.+ - |+ Other commands do return a nontrivial value after executing.+ For example, `grab` has type `cmd text`, and returns the name of the+ grabbed entity as a text value.+ - |+ To use the result of a command later, you need "bind notation", which+ consists of a variable name and a leftwards-pointing arrow+ before the command. For example:+ - |+ move; t <- grab; place t+ - |+ In the above example, the result returned by `grab` is assigned+ to the variable name `t`, which can then be used later.+ This is useful, for example, if you do not care what you+ grabbed and just want to move it to another cell, or if you+ are not sure of the name of the thing being grabbed.+ - |+ Reminder: if you need to start over, your previous commands are+ accessible in the REPL history (up-arrow).+ condition: |+ try {+ p <- robotnamed "pedestal";+ w <- as p {ishere "Hastur"};+ return w;+ } { return false }+ prerequisite: grab_artifact+solution: |+ run "data/scenarios/Tutorials/bind2-solution.sw"+entities:+ - name: Hastur+ display:+ attr: gold+ char: 'H'+ description:+ - The Unspeakable One+ properties: [portable]+robots:+ - name: base+ dir: [0,1]+ display:+ char: Ω+ attr: robot+ devices:+ - 3D printer+ inventory:+ - [1, solar panel]+ - [1, treads]+ - [1, grabber]+ - name: floorspot+ system: true+ - name: pedestal+ system: true+world:+ default: [blank]+ palette:+ '.': [blank, null]+ 'Ω': [blank, null, base]+ 'x': [dirt, null]+ 'H': [blank, Hastur, floorspot]+ 's': [stone, null, pedestal]+ '┌': [stone, upper left corner]+ '┐': [stone, upper right corner]+ '└': [stone, lower left corner]+ '┘': [stone, lower right corner]+ '─': [stone, horizontal wall]+ '│': [stone, vertical wall]+ upperleft: [-1, -3]+ map: |+ ┌─────────┐+ │.........│+ │...xxx...│+ │...xsx...│+ │...xxxH..│+ │.........│+ │Ω┌───────┘+ └─┘........
data/scenarios/Tutorials/build.yaml view
@@ -14,11 +14,9 @@ - You can build a robot with `build {COMMANDS}`, where in place of `COMMANDS` you write the sequence of commands for the robot to execute (separated by semicolons).- - Build a robot to harvest the flower and place it next- to the water. - |- TIP: You can use the name of the flower directly ("pickerelweed"),- or you can use bind notation: `f <- harvest; ... ; place f;`.+ Build a robot to harvest the "flower" and place it next+ to the water. - | TIP: Newly built robots start out facing the same direction as their parent, which in the tutorials will always be north.@@ -26,26 +24,15 @@ try { teleport self (0,-1); turn east;- a <- ishere "pickerelweed";+ a <- ishere "flower"; move;- b <- ishere "pickerelweed";+ b <- ishere "flower"; move;- c <- ishere "pickerelweed";+ c <- ishere "flower"; return (a || b || c); } { return false }-entities:- - name: pickerelweed- display:- attr: flower- char: '*'- description:- - A small plant that grows near the water.- properties: [known, portable, growable]- # It would make sense for the plant to NOT grow away from water.- # But if the player sent a robot that grabbed it and then did not- # place it, another robot would need to salvage that robot. solution: |- build {turn right; move; move; t <- harvest; turn right; move; place t}+ build {turn right; move; move; harvest; turn right; move; place "flower"} robots: - name: base dir: [0,1]@@ -69,7 +56,7 @@ 'Ω': [grass, null, base] '.': [grass] '~': [ice, water]- '*': [grass, pickerelweed]+ '*': [grass, flower] '┌': [stone, upper left corner] '┐': [stone, upper right corner] '└': [stone, lower left corner]
data/scenarios/Tutorials/conditionals.yaml view
@@ -62,9 +62,6 @@ def pick = move; ifC (ishere VSR) {grab; return ()} {} end; def pickrow = x4 pick; turn back; x4 move end; build {- require "treads"; require "branch predictor"; require "grabber";- require "lambda"; require "scanner"; // #540- turn south; x4 (move; tL; pickrow; tL); tB; x4 move; x4 (give base VSR) } robots:
data/scenarios/Tutorials/craft.yaml view
@@ -15,6 +15,11 @@ available recipes. - Your goal is to make a "branch predictor", so you will have to make some "branch"es first.+ - |+ Note: when used after opening quotes in the REPL, the Tab key can cycle through+ possible completions of a name. E.g., type:+ - |+ > make "br[Tab][Tab] condition: | try { as base {has "branch predictor"}
data/scenarios/Tutorials/crash-secret.sw view
@@ -15,41 +15,39 @@ end; // At the beginning all robots can be given Win.-def allOK: robot -> bool = \rob.+def allOK: actor -> bool = \rob. true end; +myLoc <- whereami;+ // Try to give a robot a Win, filtering out those that were already given a Win. // The robot will also receive instructions, so it **must have a logger!**-def tryGive: text -> (robot -> bool) -> int -> cmd (robot -> bool) = \msg.\f.\i.- r <- try {- robotNumbered i;- } {- log $ "could not find robot " ++ format i;- return self- };- if (r != self && f r) {- log $ "found the robot " ++ format i;- l <- whereami;- rl <- as r {whereami}; wait 1; // WHY is this 'wait 1' required???- if (l != rl) {- log $ "the robot" ++ format i ++ "is not in my cell";- return f;+def tryGive: text -> (actor -> bool) -> cmd (actor -> bool) = \msg.+ // (b -> actor -> cmd b) -> b -> cmd b+ meetAll $ \f.\rob.+ if (not $ f rob) {+ log $ "skipping the robot " ++ format rob ++ "because it already has a Win";+ return f } {- try {- reprogram r { log msg; };- log $ "successfully reprogrammed robot " ++ format i;- give r "Win";- log $ "successfully gave Win to robot " ++ format i;+ robLoc <- as rob {whereami};+ if (robLoc != myLoc) {+ log $ "the robot" ++ format rob ++ "is not in my cell";+ return f; } {- log $ "the robot " ++ format i ++ "is missing a logger!"- };- return (\rob. (rob != r && f rob));+ try {+ reprogram rob { log msg; };+ log $ "successfully reprogrammed robot " ++ format rob;+ give rob "Win";+ log $ "successfully gave Win to robot " ++ format rob;+ return (\r. (rob != r && f rob));+ } {+ log $ "the robot " ++ format rob ++ "is missing a logger!";+ return f;+ };+ + } }- } {- log $ "skipping the robot " ++ format i;- return f- } end; // -------------------------------------------------------------------------@@ -57,7 +55,7 @@ // ------------------------------------------------------------------------- log "Hi, I am secret";-iterate allOK (foreachF 1 16 $ tryGive+iterate allOK (tryGive $ "Send a robot to `salvage` me and come back to `give base \"Win\"`.\n" ++ "When the rescue robot stands where I am and executes `salvage`,\n" ++ "all my inventory and logs will go to it, including the \"Win\".\n"
data/scenarios/Tutorials/def.yaml view
@@ -46,7 +46,6 @@ def tB = turn back end; def S = m16; tL; m2; tL; m16; tR; m2; tR; m16 end; build {- require "treads"; // #540 turn right; S; f <- harvest; tB; S; give base f } robots:
+ data/scenarios/Tutorials/equip.yaml view
@@ -0,0 +1,61 @@+version: 1+name: Equip+description: |+ Learn how to equip devices and gain new capabilities.+objectives:+ - goal:+ - You know how to `grab` things lying around, so you are ready to get+ an upgrade!+ By equipping devices, you learn new capabilities which allow you to+ perform more complex commands.+ - Before you start building new robots in the later tutorials, you need+ to gain the "build" capability.+ Try typing `build {}` - you should get an error telling you that you+ need to equip a "3D printer".+ - |+ Fortunately, there is a 3D printer lying nearby. Go `grab` it, then+ equip it with `equip "3D printer"`.+ - |+ You win by building your first robot:+ - |+ build {}+ condition: |+ try {+ _ <- robotNumbered 1;+ return true;+ } { return false }+solution: |+ turn south; move; grab; equip "3D printer"; build {};+robots:+ - name: base+ dir: [1,0]+ devices:+ - logger+ - treads+ - compass+ - grabber+ - welder+ inventory:+ - [10, solar panel]+ - [10, logger]+known: [3D printer, water]+world:+ default: [blank]+ palette:+ '>': [grass, null, base]+ '.': [grass]+ '~': [ice, water]+ '3': [grass, 3D printer]+ '┌': [stone, upper left corner]+ '┐': [stone, upper right corner]+ '└': [stone, lower left corner]+ '┘': [stone, lower right corner]+ '─': [stone, horizontal wall]+ '│': [stone, vertical wall]+ upperleft: [-1, 1]+ map: |+ ┌───┐+ │>..│+ │3..│+ │~~~│+ └───┘
data/scenarios/Tutorials/farming.sw view
@@ -11,7 +11,7 @@ def while : cmd bool -> {cmd a} -> cmd unit = \test. \body. ifC test {force body ; while test body} {} end;-def giveall : robot -> text -> cmd unit = \r. \thing.+def giveall : actor -> text -> cmd unit = \r. \thing. while (has thing) {give r thing} end; def x4 = \c. c; c; c; c end;@@ -20,7 +20,6 @@ def m12 = x12 move end; def next_row = tB; m12; tL; move; tL end; def plant_field : text -> cmd unit = \thing.- log "planting"; x4 ( x12 (move; place thing; harvest); next_row@@ -39,14 +38,9 @@ ) end; build {- require "treads"; require "harvester"; require "logger";- require "lambda"; require "branch predictor"; // #540 require 1 "lambda"; tB; move; tR; plant_field "lambda"; }; build {- require "treads"; require "harvester"; require "logger"; require "scanner";- require "grabber";- require "lambda"; require "branch predictor"; require "strange loop"; // #540 harvest_lambdas }
data/scenarios/Tutorials/farming.yaml view
@@ -3,7 +3,9 @@ description: | Learn how to build a farm to grow and harvest items. objectives:- - goal:+ - id: get_many_lambdas+ teaser: Get 256 lambdas+ goal: - Lambdas are an essential item for building robots, but they are somewhat rare in the wild. Therefore, it makes sense to farm them in order to create a reliable supply.@@ -47,6 +49,7 @@ - Now go forth and build your swarm! condition: | try {as base {has "curry"}} {return false}+ prerequisite: get_many_lambdas solution: | run "scenarios/Tutorials/farming.sw"; run "scenarios/Tutorials/make_curry.sw";@@ -61,6 +64,7 @@ - 3D printer - dictionary - grabber+ - welder - life support system - logger - toolkit
− data/scenarios/Tutorials/install.yaml
@@ -1,60 +0,0 @@-version: 1-name: Install-description: |- Learn how to install devices and gain new capabilities.-objectives:- - goal:- - You know how to `grab` things lying around, so you are ready to get- an upgrade!- By installing devices, you learn new capabilities which allow you to- perform more complex commands.- - Before you start building new robots in the later tutorials, you need- to gain the "build" capability.- Try typing `build {}` - you should get an error telling you that you- need to install a "3D printer".- - |- Fortunately, there is a 3D printer lying nearby. Go `grab` it, then- install it on yourself with `install base "3D printer"`.- - |- You win by building your first robot:- - |- build {log "Hello World!"}- condition: |- try {- _ <- robotNumbered 1;- return true;- } { return false }-solution: |- turn south; move; grab; install base "3D printer"; build {log "Hello World!"};-robots:- - name: base- dir: [1,0]- devices:- - logger- - treads- - compass- - grabber- inventory:- - [10, solar panel]- - [10, logger]-known: [3D printer, water]-world:- default: [blank]- palette:- '>': [grass, null, base]- '.': [grass]- '~': [ice, water]- '3': [grass, 3D printer]- '┌': [stone, upper left corner]- '┐': [stone, upper right corner]- '└': [stone, lower left corner]- '┘': [stone, lower right corner]- '─': [stone, horizontal wall]- '│': [stone, vertical wall]- upperleft: [-1, 1]- map: |- ┌───┐- │>..│- │3..│- │~~~│- └───┘
data/scenarios/Tutorials/lambda.yaml view
@@ -34,7 +34,6 @@ def tL = turn left end; def s = m4; tL; m2; tL; m4; tR; m2; tR end; build {- require "treads"; require "lambda"; // #540 turn right; x4 (x4 s; m4; m2; tR; x4 (x4 move); tL; m2); grab
data/scenarios/Tutorials/move.yaml view
@@ -3,7 +3,9 @@ description: | Learn how to move and chain commands. objectives:- - goal:+ - id: move_to_first_flower+ teaser: Get the flower+ goal: - Robots can use the `move` command to move forward one unit in the direction they are currently facing. - To complete this challenge, move your robot two spaces to the right,@@ -14,7 +16,9 @@ condition: | r <- robotNamed "check1"; loc <- as r {has "Win"};- - goal:+ - id: move_along_corridor+ teaser: Down the corridor+ goal: - Good! Now you need to learn how to effectively repeat actions. - | Previously you could move twice by chaining the move command:@@ -29,7 +33,10 @@ condition: | r <- robotNamed "check2"; loc <- as r {has "Win"};- - goal:+ prerequisite: move_to_first_flower+ - id: move_northeast_corner+ teaser: To northeast corner+ goal: - Well done! In addition to `move`, you can use the `turn` command to turn your robot, for example, `turn right` or `turn east`. - Switch to the inventory view in the upper left (by clicking on it or typing `Alt+E`)@@ -51,6 +58,7 @@ condition: | r <- robotNamed "check3"; loc <- as r {has "Win"};+ prerequisite: move_along_corridor - goal: - Good job! You are now ready to move and turn on your own. - To complete this challenge, move your robot to the northeast corner,@@ -61,6 +69,7 @@ condition: | r <- robotNamed "check4"; loc <- as r {has "Win"};+ prerequisite: move_northeast_corner solution: | // 0 move;move;
data/scenarios/Tutorials/require.yaml view
@@ -1,24 +1,25 @@ version: 1 name: Require devices description: |- Learn how to require additional devices that would otherwise not be installed.+ Learn how to require additional devices that would otherwise not be equipped. objectives: - goal:- - The `build` command automatically installs devices that it knows+ - The `build` command automatically equips devices on the newly+ built robot that it knows will be required. For example, if you `build {move}`, some `treads`- will automatically be installed on the new robot since it needs+ will automatically be equipped on the new robot since it needs them to `move`. - However, sometimes you need a device but `build` can't tell that you need it. In this case, you can use the special `require` command to require a particular device. For example, if you `build {require "3D printer"; move}`, a 3D printer will be- installed on the new robot even though it does not execute any+ equipped on the new robot even though it does not execute any commands that use a 3D printer. - Your goal is to pick a flower on the other side of the river and bring it back to your base. You win when the base has a `periwinkle` flower in its inventory. - "Hint: robots will drown in the water unless they have a `boat` device- installed!"+ equipped!" condition: | try { as base {has "periwinkle"}@@ -34,8 +35,6 @@ solution: | def m5 = move; move; move; move; move end; build {- require "treads"; // #540- require "boat"; turn right; m5; f <- grab; turn back; m5; give base f }
data/scenarios/Tutorials/requireinv.yaml view
@@ -5,8 +5,8 @@ objectives: - goal: - In the previous tutorial challenge, you learned how to use- `require` to require specific devices to be installed.- Sometimes, instead of requiring installed devices, you require+ `require` to require specific devices to be equipped.+ Sometimes, instead of requiring equipped devices, you require supplies in your inventory. In this case, you can write `require <int> <name>` to require a certain number of copies of a certain entity to be placed in your inventory.@@ -38,7 +38,6 @@ def mp = move; place "rock" end; def rr = turn right; move; turn right; x4 mp; turn back; x4 move end; build {- require "treads"; require "grabber"; require "lambda"; // #540 require 16 "rock"; x4 rr };
data/scenarios/Tutorials/world101.yaml view
@@ -3,14 +3,16 @@ description: | Learn some of the first steps you might take on the new planet. objectives:- - goal:+ - id: get_trees+ teaser: Get 3 trees+ goal: - You're ready to graduate to something a bit more complex! This multi-step tutorial will walk you through a few of the first steps you might take in exploring the new planet, using many of the commands and techniques you have learned in the previous tutorial challenges. - You can see that your base starts out with some key devices- installed and some basic supplies for building robots. To+ equipped and some basic supplies for building robots. To build more advanced devices and produce more robots, you'll need to explore, gather resources, and set up some automated production pipelines.@@ -26,7 +28,9 @@ n <- as base {count "tree"}; return (n >= 3) } { return false }- - goal:+ - id: get_harvester+ teaser: Make a harvester+ goal: - Nice work! Now, use the trees to make a harvester device. This will require several intermediate products; try making various things, and take a look at your available recipes (F3)@@ -34,6 +38,7 @@ you may end up needing some additional trees. condition: | try { as base {has "harvester"} } {return false}+ prerequisite: get_trees - goal: - Now that you have a harvester, you can use `harvest` instead of `grab` whenever you pick up a growing item (check for the word "growing" at the@@ -50,6 +55,7 @@ coordinates." condition: | try { as base {has "lambda"} } {return false}+ prerequisite: get_harvester robots: - name: base display:@@ -61,6 +67,7 @@ - 3D printer - dictionary - grabber+ - welder - life support system - logger - toolkit
data/scenarios/classic.yaml view
@@ -13,6 +13,7 @@ - 3D printer - dictionary - grabber+ - welder - life support system - logger - toolkit
+ data/test/language-snippets/warnings/unused-vars/multiple-lambda-first-unused.sw view
@@ -0,0 +1,3 @@+def put = \x. \y.+ place y;+ end;
+ data/test/language-snippets/warnings/unused-vars/multiple-lambda-second-unused.sw view
@@ -0,0 +1,3 @@+def put = \x. \y.+ place x;+ end;
+ data/test/language-snippets/warnings/unused-vars/multiple-let-all-unused.sw view
@@ -0,0 +1,3 @@+let x = "foo" in+let y = "bar" in+place "quux";
+ data/test/language-snippets/warnings/unused-vars/multiple-let-first-unused.sw view
@@ -0,0 +1,3 @@+let x = "foo" in+let y = "bar" in+place y;
+ data/test/language-snippets/warnings/unused-vars/multiple-let-second-unused.sw view
@@ -0,0 +1,3 @@+let x = "foo" in+let y = "bar" in+place x;
+ data/test/language-snippets/warnings/unused-vars/recursive-let.sw view
@@ -0,0 +1,1 @@+let one = 1 in let fac = \n. if (n == 0) {one} {n * fac (n-1)} in 17
+ data/test/language-snippets/warnings/unused-vars/shadowed-variable-lambda-unused.sw view
@@ -0,0 +1,1 @@+def y = \x. \x. x + 1 end
+ data/test/language-snippets/warnings/unused-vars/shadowed-variable-let-intermediate-use.sw view
@@ -0,0 +1,4 @@+let x = "flower" in+place x;+x <- grab;+give parent x;
+ data/test/language-snippets/warnings/unused-vars/shadowed-variable-let-unused.sw view
@@ -0,0 +1,3 @@+let x = "flower" in+x <- grab;+give parent x;
+ data/test/language-snippets/warnings/unused-vars/single-bind-unused.sw view
@@ -0,0 +1,2 @@+x <- grab;+place "foo";
+ data/test/language-snippets/warnings/unused-vars/single-bind-used.sw view
@@ -0,0 +1,2 @@+x <- grab;+place x;
editors/emacs/swarm-mode.el view
@@ -45,6 +45,8 @@ "format" "chars" "split"+ "charat"+ "tochar" )) (x-commands '( "noop"@@ -56,10 +58,11 @@ "harvest" "place" "give"- "install"+ "equip"+ "unequip" "make" "has"- "installed"+ "equipped" "count" "drill" "build"@@ -73,10 +76,14 @@ "create" "time" "whereami"+ "heading" "blocked" "scan" "upload" "ishere"+ "isempty"+ "meet"+ "meetall" "whoami" "setname" "random"@@ -90,17 +97,17 @@ "robotnamed" "robotnumbered" "knows"- "left"- "right"- "back"- "forward" "north" "south" "east" "west"+ "left"+ "right"+ "back"+ "forward" "down" ))- (x-types '("int" "text" "dir" "bool" "cmd" "void" "unit"))+ (x-types '("int" "text" "dir" "bool" "cmd" "void" "unit" "actor")) (x-keywords-regexp (regexp-opt x-keywords 'words)) (x-builtins-regexp (regexp-opt x-builtins 'words))
editors/vscode/syntaxes/swarm.tmLanguage.json view
@@ -29,7 +29,7 @@ }, { "name": "keyword.control.dictionary.let",- "begin": "\\s*let\\s+(\\w+)\\s*(:((\\s*(cmd|dir|text|int|void|unit|\\(|\\)|\\{|\\}|(\\*|\\+|->)|[a-z]\\w*|forall ([a-z]\\w*\\s*)+.)\\s*)+))?=",+ "begin": "\\s*let\\s+(\\w+)\\s*(:((\\s*(cmd|dir|text|int|void|unit|actor|\\(|\\)|\\{|\\}|(\\*|\\+|->)|[a-z]\\w*|forall ([a-z]\\w*\\s*)+.)\\s*)+))?=", "end": "\\s*in", "beginCaptures": { "1": {"name": "entity.name.function"},@@ -58,7 +58,7 @@ }, { "name": "keyword.other",- "match": "\\b(?i)(self|parent|base|if|inl|inr|case|fst|snd|force|undefined|fail|not|format|chars|split|noop|wait|selfdestruct|move|turn|grab|harvest|place|give|install|make|has|installed|count|drill|build|salvage|reprogram|say|listen|log|view|appear|create|time|whereami|blocked|scan|upload|ishere|whoami|setname|random|run|return|try|swap|atomic|teleport|as|robotnamed|robotnumbered|knows)\\b"+ "match": "\\b(?i)(self|parent|base|if|inl|inr|case|fst|snd|force|undefined|fail|not|format|chars|split|charat|tochar|noop|wait|selfdestruct|move|turn|grab|harvest|place|give|equip|unequip|make|has|equipped|count|drill|build|salvage|reprogram|say|listen|log|view|appear|create|time|whereami|heading|blocked|scan|upload|ishere|isempty|meet|meetall|whoami|setname|random|run|return|try|swap|atomic|teleport|as|robotnamed|robotnumbered|knows)\\b" } ] },@@ -89,7 +89,7 @@ "patterns": [ { "name": "variable.language.dir",- "match": "\\b(?i)(left|right|back|forward|north|south|east|west|down)\\b"+ "match": "\\b(?i)(north|south|east|west|left|right|back|forward|down)\\b" }, { "name": "variable.parameter",
example/BFS-clear.sw view
@@ -63,4 +63,4 @@ give base "tree"; selfdestruct; end;-def start : cmd robot = build {turn west; repeat 7 move; clear} end+def start : cmd actor = build {turn west; repeat 7 move; clear} end
example/list.sw view
@@ -148,7 +148,7 @@ // nil : listI def nil = 0 end -// Add non-negative number to beggining of list (cons adds the sign)+// Add non-negative number to beginning of list (cons adds the sign) // consP : nat -> listI -> int def consP = \x.\xs. if (x == 0)@@ -159,7 +159,7 @@ } end -// Add integer to the beggining of the list+// Add integer to the beginning of the list // consP : int -> listI -> listI def cons = \x.\xs. if (x >= 0)
+ src/Data/BoolExpr.hs view
@@ -0,0 +1,337 @@+{-+Copyright (c) 2008, 2009, Nicolas Pouillard+All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are+met:++ * Redistributions of source code must retain the above copyright+ notice, this list of conditions and the following disclaimer.++ * Redistributions in binary form must reproduce the above+ copyright notice, this list of conditions and the following+ disclaimer in the documentation and/or other materials provided+ with the distribution.++ * Neither the name of the copyright holders nor the names of other+ contributors may be used to endorse or promote products derived+ from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.+-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE NoDeriveAnyClass #-}++--------------------------------------------------------------------++--------------------------------------------------------------------++-- |+-- Module : Data.BoolExpr+-- Copyright : (c) Nicolas Pouillard 2008,2009+-- License : BSD3+--+-- Maintainer: Nicolas Pouillard <nicolas.pouillard@gmail.com>+-- Stability : provisional+-- Portability:+--+-- Boolean expressions and various representations.+module Data.BoolExpr (+ -- * A boolean class+ Boolean (..),++ -- * Boolean trees+ BoolExpr (..),+ reduceBoolExpr,+ evalBoolExpr,++ -- * Boolean evaluation semantic+ Eval (..),+ runEvalId,++ -- * Signed constants+ Signed (..),+ negateSigned,+ evalSigned,+ constants,+ negateConstant,++ -- * Conjunctive Normal Form+ CNF (..),+ Conj (..),+ boolTreeToCNF,++ -- * Disjunctive Normal Form+ Disj (..),+ DNF (..),+ boolTreeToDNF,++ -- * Other transformations+ dualize,+ fromBoolExpr,+ pushNotInwards,+)+where++import Control.Monad (ap)+import Data.Aeson+import Data.Char (toLower)+import Data.Traversable+import GHC.Generics (Generic)++-- | Signed values are either positive or negative.+data Signed a = Positive a | Negative a+ deriving (Eq, Ord, Generic, Show, Read)++instance Functor Signed where+ fmap f (Positive x) = Positive (f x)+ fmap f (Negative x) = Negative (f x)++instance Traversable Signed where+ traverse f (Positive x) = Positive <$> f x+ traverse f (Negative x) = Negative <$> f x++instance Foldable Signed where+ foldMap = foldMapDefault++instance Applicative Signed where+ pure = Positive+ (<*>) = ap++instance Monad Signed where+ Positive x >>= f = f x+ Negative x >>= f = negateSigned $ f x++infix 9 /\+infix 9 \/++-- | A boolean type class.+class Boolean f where+ (/\) :: f a -> f a -> f a+ (\/) :: f a -> f a -> f a+ bNot :: f a -> f a+ bTrue :: f a+ bFalse :: f a+ bConst :: Signed a -> f a++-- | Syntax of boolean expressions parameterized over a+-- set of leaves, named constants.+data BoolExpr a+ = BAnd (BoolExpr a) (BoolExpr a)+ | BOr (BoolExpr a) (BoolExpr a)+ | BNot (BoolExpr a)+ | BTrue+ | BFalse+ | BConst (Signed a)+ deriving (Eq, Ord, Generic, Show {-! derive : Arbitrary !-})++encodingOptions :: Options+encodingOptions =+ defaultOptions+ { sumEncoding = ObjectWithSingleField+ , constructorTagModifier = map toLower+ }++instance (ToJSON a) => ToJSON (Signed a) where+ toJSON = genericToJSON encodingOptions++instance (ToJSON a) => ToJSON (BoolExpr a) where+ toJSON = genericToJSON encodingOptions++instance (ToJSON a) => ToJSON (DNF a) where+ toJSON = genericToJSON encodingOptions++instance (ToJSON a) => ToJSON (CNF a) where+ toJSON = genericToJSON encodingOptions++instance (ToJSON a) => ToJSON (Conj a) where+ toJSON = genericToJSON encodingOptions++instance (ToJSON a) => ToJSON (Disj a) where+ toJSON = genericToJSON encodingOptions++instance Functor BoolExpr where+ fmap f (BAnd a b) = BAnd (fmap f a) (fmap f b)+ fmap f (BOr a b) = BOr (fmap f a) (fmap f b)+ fmap f (BNot t) = BNot (fmap f t)+ fmap _ BTrue = BTrue+ fmap _ BFalse = BFalse+ fmap f (BConst x) = BConst (fmap f x)++instance Traversable BoolExpr where+ traverse f (BAnd a b) = BAnd <$> traverse f a <*> traverse f b+ traverse f (BOr a b) = BOr <$> traverse f a <*> traverse f b+ traverse f (BNot t) = BNot <$> traverse f t+ traverse _ BTrue = pure BTrue+ traverse _ BFalse = pure BFalse+ traverse f (BConst x) = BConst <$> traverse f x++instance Foldable BoolExpr where+ foldMap = foldMapDefault++instance Boolean BoolExpr where+ (/\) = BAnd+ (\/) = BOr+ bNot = BNot+ bTrue = BTrue+ bFalse = BFalse+ bConst = BConst++newtype Eval b a = Eval {runEval :: (a -> b) -> b}++runEvalId :: Eval a a -> a+runEvalId e = runEval e id++instance b ~ Bool => Boolean (Eval b) where+ (/\) = liftE2 (&&)+ (\/) = liftE2 (||)+ bNot = liftE not+ bTrue = Eval $ const True+ bFalse = Eval $ const False+ bConst = Eval . flip evalSigned++liftE :: (b -> b) -> Eval b a -> Eval b a+liftE f (Eval x) = Eval (f . x)++liftE2 :: (b -> b -> b) -> Eval b a -> Eval b a -> Eval b a+liftE2 f (Eval x) (Eval y) = Eval (\e -> f (x e) (y e))++-- | Turns a boolean tree into any boolean type.+fromBoolExpr :: Boolean f => BoolExpr a -> f a+fromBoolExpr (BAnd l r) = fromBoolExpr l /\ fromBoolExpr r+fromBoolExpr (BOr l r) = fromBoolExpr l \/ fromBoolExpr r+fromBoolExpr (BNot t) = bNot $ fromBoolExpr t+fromBoolExpr BTrue = bTrue+fromBoolExpr BFalse = bFalse+fromBoolExpr (BConst c) = bConst c++--- | Disjunction of atoms ('a')+newtype Disj a = Disj {unDisj :: [a]}+ deriving (Show, Generic, Functor, Semigroup, Monoid)++--- | Conjunction of atoms ('a')+newtype Conj a = Conj {unConj :: [a]}+ deriving (Show, Generic, Functor, Semigroup, Monoid)++--- | Conjunctive Normal Form+newtype CNF a = CNF {unCNF :: Conj (Disj (Signed a))}+ deriving (Show, Generic, Semigroup, Monoid)++--- | Disjunctive Normal Form+newtype DNF a = DNF {unDNF :: Disj (Conj (Signed a))}+ deriving (Show, Generic, Semigroup, Monoid)++instance Functor CNF where+ fmap f (CNF x) = CNF (fmap (fmap (fmap f)) x)++instance Boolean CNF where+ l /\ r = l `mappend` r+ l \/ r =+ CNF $+ Conj+ [ x `mappend` y | x <- unConj $ unCNF l, y <- unConj $ unCNF r+ ]+ bNot = error "bNot on CNF"+ bTrue = CNF $ Conj []+ bFalse = CNF $ Conj [Disj []]+ bConst x = CNF $ Conj [Disj [x]]++instance Functor DNF where+ fmap f (DNF x) = DNF (fmap (fmap (fmap f)) x)++instance Boolean DNF where+ l /\ r =+ DNF $+ Disj+ [ x `mappend` y | x <- unDisj $ unDNF l, y <- unDisj $ unDNF r+ ]+ l \/ r = l `mappend` r+ bNot = error "bNot on CNF"+ bTrue = DNF $ Disj [Conj []]+ bFalse = DNF $ Disj []+ bConst x = DNF $ Disj [Conj [x]]++-- | Reduce a boolean tree annotated by booleans to a single boolean.+reduceBoolExpr :: BoolExpr Bool -> Bool+reduceBoolExpr = evalBoolExpr id++-- Given a evaluation function of constants, returns an evaluation+-- function over boolean trees.+--+-- Note that since 'BoolExpr' is a functor, one can simply use+-- 'reduceBoolExpr':+--+-- @+-- evalBoolExpr f = reduceBoolExpr . fmap (f$)+-- @+evalBoolExpr :: (a -> Bool) -> (BoolExpr a -> Bool)+evalBoolExpr env expr = runEval (fromBoolExpr expr) env++-- | Returns constants used in a given boolean tree, these+-- constants are returned signed depending one how many+-- negations stands over a given constant.+constants :: BoolExpr a -> [Signed a]+constants = go True+ where+ go sign (BAnd a b) = go sign a ++ go sign b+ go sign (BOr a b) = go sign a ++ go sign b+ go sign (BNot t) = go (not sign) t+ go _ BTrue = []+ go _ BFalse = []+ go sign (BConst x) = [if sign then x else negateSigned x]++dualize :: Boolean f => BoolExpr a -> f a+dualize (BAnd l r) = dualize l \/ dualize r+dualize (BOr l r) = dualize l /\ dualize r+dualize BTrue = bFalse+dualize BFalse = bTrue+dualize (BConst c) = negateConstant c+dualize (BNot e) = fromBoolExpr e++-- When dualize is used by pushNotInwards not BNot remain,+-- hence it makes sense to assert that dualize does not+-- have to work on BNot. However `dualize` can be freely+-- used as a fancy `bNot`.+-- dualize (BNot _) = error "dualize: impossible"++-- | Push the negations inwards as much as possible.+-- The resulting boolean tree no longer use negations.+pushNotInwards :: Boolean f => BoolExpr a -> f a+pushNotInwards (BAnd l r) = pushNotInwards l /\ pushNotInwards r+pushNotInwards (BOr l r) = pushNotInwards l \/ pushNotInwards r+pushNotInwards (BNot t) = dualize $ pushNotInwards t+pushNotInwards BTrue = bTrue+pushNotInwards BFalse = bFalse+pushNotInwards (BConst c) = bConst c++-- | Convert a boolean tree to a conjunctive normal form.+boolTreeToCNF :: BoolExpr a -> CNF a+boolTreeToCNF = pushNotInwards++-- | Convert a boolean tree to a disjunctive normal form.+boolTreeToDNF :: BoolExpr a -> DNF a+boolTreeToDNF = pushNotInwards++evalSigned :: (a -> Bool) -> Signed a -> Bool+evalSigned f (Positive x) = f x+evalSigned f (Negative x) = not $ f x++negateSigned :: Signed a -> Signed a+negateSigned (Positive x) = Negative x+negateSigned (Negative x) = Positive x++negateConstant :: Boolean f => Signed a -> f a+negateConstant = bConst . negateSigned
+ src/Data/BoolExpr/Simplify.hs view
@@ -0,0 +1,57 @@+-- | Simplification logic for boolean expressions that is not+-- provided in the 'boolexpr' package.+module Data.BoolExpr.Simplify (+ cannotBeTrue,+ replace,+) where++import Data.BoolExpr+import Data.List qualified as L+import Data.Map (Map)+import Data.Map qualified as M+import Data.Set qualified as S++-- | Used only by "hasContradiction".+-- Note that the Booleans returned in this tuple are not actually used+-- as conditions, and therefore their semantic convention (e.g. associating+-- True = Positive and False = Negative) is irrelevant.+-- Rather, they are collected into sets+-- to determine whether both True and False exist for a key.+extractConstFromSigned :: Signed a -> (a, Bool)+extractConstFromSigned v = case v of+ Negative x -> (x, False)+ Positive x -> (x, True)++hasContradiction :: Ord a => Conj (Signed a) -> Bool+hasContradiction (Conj items) =+ not $+ M.null $+ M.filter ((> 1) . S.size) $+ M.fromListWith (<>) $+ fmap (fmap S.singleton . extractConstFromSigned) items++simplifyDNF :: Ord a => DNF a -> DNF a+simplifyDNF (DNF (Disj disjunctions)) =+ DNF $ Disj $ L.filter (not . hasContradiction) disjunctions++isAlwaysFalse :: Ord a => DNF a -> Bool+isAlwaysFalse (DNF (Disj disjunctions)) = L.null disjunctions++cannotBeTrue :: Ord a => BoolExpr a -> Bool+cannotBeTrue = isAlwaysFalse . simplifyDNF . boolTreeToDNF++replace :: Ord a => Map a Bool -> BoolExpr a -> BoolExpr a+replace f (BAnd a b) = BAnd (replace f a) (replace f b)+replace f (BOr a b) = BOr (replace f a) (replace f b)+replace f (BNot t) = BNot (replace f t)+replace _ BTrue = BTrue+replace _ BFalse = BFalse+replace m c@(BConst x) = case M.lookup varname m of+ Nothing -> c+ Just val ->+ if txform val+ then BTrue+ else BFalse+ where+ (varname, isPositive) = extractConstFromSigned x+ txform = if isPositive then id else not
src/Swarm/App.hs view
@@ -13,16 +13,17 @@ import Brick import Brick.BChan import Control.Concurrent (forkIO, threadDelay)-import Control.Lens ((%~), (&), (?~), (^.))+import Control.Lens (view, (%~), (&), (?~)) import Control.Monad.Except import Data.IORef (newIORef, writeIORef) import Data.Text qualified as T import Data.Text.IO qualified as T import Graphics.Vty qualified as V-import Swarm.Game.Robot (LogSource (ErrorTrace, Said))-import Swarm.TUI.Attr+import Swarm.Game.Robot (ErrorLevel (..), LogSource (ErrorTrace, Said)) import Swarm.TUI.Controller import Swarm.TUI.Model+import Swarm.TUI.Model.StateUpdate+import Swarm.TUI.Model.UI (uiAttrMap) import Swarm.TUI.View import Swarm.Version (getNewerReleaseVersion) import Swarm.Web@@ -38,7 +39,7 @@ , appChooseCursor = chooseCursor , appHandleEvent = eventHandler , appStartEvent = enablePasteMode- , appAttrMap = const swarmAttrMap+ , appAttrMap = view $ uiState . uiAttrMap } -- | The main @IO@ computation which initializes the state, sets up@@ -74,27 +75,30 @@ writeBChan chan (UpstreamVersion upRel) -- Start the web service with a reference to the game state- gsRef <- newIORef (s ^. gameState)- eport <- Swarm.Web.startWebThread (userWebPort opts) gsRef+ appStateRef <- newIORef s+ eport <- Swarm.Web.startWebThread (userWebPort opts) appStateRef let logP p = logEvent Said ("Web API", -2) ("started on :" <> T.pack (show p))- let logE e = logEvent ErrorTrace ("Web API", -2) (T.pack e)+ let logE e = logEvent (ErrorTrace Error) ("Web API", -2) (T.pack e) let s' =- s & runtimeState- %~ case eport of- Right p -> (webPort ?~ p) . (eventLog %~ logP p)- Left e -> eventLog %~ logE e+ s+ & runtimeState+ %~ case eport of+ Right p -> (webPort ?~ p) . (eventLog %~ logP p)+ Left e -> eventLog %~ logE e -- Update the reference for every event let eventHandler e = do curSt <- get- liftIO $ writeIORef gsRef (curSt ^. gameState)+ liftIO $ writeIORef appStateRef curSt handleEvent e - -- Run the app.- let buildVty = V.mkVty V.defaultConfig+ -- Setup virtual terminal+ let buildVty = V.mkVty $ V.defaultConfig {V.colorMode = colorMode opts} initialVty <- buildVty V.setMode (V.outputIface initialVty) V.Mouse True++ -- Run the app. void $ customMain initialVty buildVty (Just chan) (app eventHandler) s' -- | A demo program to run the web service directly, without the terminal application.@@ -111,14 +115,15 @@ , scriptToRun = Nothing , autoPlay = False , cheatMode = False+ , colorMode = Nothing , userWebPort = Nothing , repoGitInfo = Nothing } case res of Left errMsg -> T.putStrLn errMsg Right s -> do- gsRef <- newIORef (s ^. gameState)- webMain Nothing demoPort gsRef+ appStateRef <- newIORef s+ webMain Nothing demoPort appStateRef where demoScenario = Just "./data/scenarios/Testing/475-wait-one.yaml"
src/Swarm/DocGen.hs view
@@ -45,7 +45,7 @@ import Swarm.Game.Entity (Entity, EntityMap (entitiesByName), entityDisplay, entityName, loadEntities) import Swarm.Game.Entity qualified as E import Swarm.Game.Recipe (Recipe, loadRecipes, recipeInputs, recipeOutputs, recipeRequirements, recipeTime, recipeWeight)-import Swarm.Game.Robot (installedDevices, instantiateRobot, robotInventory)+import Swarm.Game.Robot (equippedDevices, instantiateRobot, robotInventory) import Swarm.Game.Scenario (Scenario, loadScenario, scenarioRobots) import Swarm.Game.WorldGen (testWorld2Entites) import Swarm.Language.Capability (Capability)@@ -54,7 +54,7 @@ import Swarm.Language.Syntax (Const (..)) import Swarm.Language.Syntax qualified as Syntax import Swarm.Language.Typecheck (inferConst)-import Swarm.Util (getDataFileNameSafe, isRightOr)+import Swarm.Util (getDataFileNameSafe, isRightOr, listEnums, quote) import Text.Dot (Dot, NodeId, (.->.)) import Text.Dot qualified as Dot import Witch (from)@@ -105,7 +105,7 @@ putStrLn $ "-- " <> show et putStrLn $ replicate 40 '-' generateEditorKeywords et- mapM_ editorGen [minBound .. maxBound]+ mapM_ editorGen listEnums CheatSheet address s -> case s of Nothing -> error "Not implemented for all Wikis" Just st -> case st of@@ -161,8 +161,6 @@ editorList = \case Emacs -> T.unlines . map ((" " <>) . quote) VSCode -> T.intercalate "|"- where- quote = T.cons '"' . flip T.snoc '"' constSyntax :: Const -> Text constSyntax = Syntax.syntax . Syntax.constInfo@@ -306,7 +304,7 @@ header = [listToRow mw capabilityHeader, separatingLine mw] capabilityPage :: PageAddress -> EntityMap -> Text-capabilityPage a em = capabilityTable a em [minBound .. maxBound]+capabilityPage a em = capabilityTable a em listEnums -- --------- -- Entities@@ -321,7 +319,7 @@ escapeTable [ codeQuote . T.singleton $ e ^. entityDisplay . to displayChar , addLink ("#" <> linkID) $ view entityName e- , T.intercalate ", " $ Capability.capabilityName <$> view E.entityCapabilities e+ , T.intercalate ", " $ Capability.capabilityName <$> Set.toList (view E.entityCapabilities e) , T.intercalate ", " . map tshow . filter (/= E.Portable) $ toList props , if E.Portable `elem` props then ":heavy_check_mark:"@@ -351,7 +349,7 @@ <> [T.intercalate "\n\n" $ view E.entityDescription e] where props = view E.entityProperties e- caps = view E.entityCapabilities e+ caps = Set.toList $ view E.entityCapabilities e entitiesPage :: PageAddress -> [Entity] -> Text entitiesPage _a es =@@ -415,9 +413,9 @@ world <- diamond "World" base <- diamond "Base" -- --------------------------------------------------------------------------- -- add nodes with for all the known entites+ -- add nodes with for all the known entities let enames' = toList . Map.keysSet . entitiesByName $ emap- enames = filter (`Set.notMember` ignoredEntites) enames'+ enames = filter (`Set.notMember` ignoredEntities) enames' ebmap <- Map.fromList . zip enames <$> mapM (box . unpack) enames -- -------------------------------------------------------------------------- -- getters for the NodeId based on entity name or the whole entity@@ -425,7 +423,7 @@ getE = safeGetEntity ebmap nid = getE . view entityName -- --------------------------------------------------------------------------- -- Get the starting inventories, entites present in the world and compute+ -- Get the starting inventories, entities present in the world and compute -- how hard each entity is to get - see 'recipeLevels'. let devs = startingDevices classic inv = startingInventory classic@@ -439,13 +437,13 @@ mapM_ ((base ---<>) . nid) devs mapM_ ((base .->.) . nid . fst) $ Map.toList inv -- --------------------------------------------------------------------------- -- World entites+ -- World entities (_wc, ()) <- Dot.cluster $ do Dot.attribute ("style", "filled") Dot.attribute ("color", "forestgreen") mapM_ ((uncurry (Dot..->.) . (world,)) . getE) (toList testWorld2Entites) -- --------------------------------------------------------------------------- let -- put a hidden node above and below entites and connect them by hidden edges+ let -- put a hidden node above and below entities and connect them by hidden edges wrapBelowAbove :: Set Entity -> Dot (NodeId, NodeId) wrapBelowAbove ns = do b <- hiddenNode@@ -454,7 +452,7 @@ mapM_ (b .~>.) ns' mapM_ (.~>. t) ns' return (b, t)- -- put set of entites in nice+ -- put set of entities in nice subLevel :: Int -> Set Entity -> Dot (NodeId, NodeId) subLevel i ns = fmap snd . Dot.cluster $ do Dot.attribute ("style", "filled")@@ -471,7 +469,7 @@ ] return bt -- --------------------------------------------------------------------------- -- order entites into clusters based on how "far" they are from+ -- order entities into clusters based on how "far" they are from -- what is available at the start - see 'recipeLevels'. bottom <- wrapBelowAbove worldEntites ls <- zipWithM subLevel [1 ..] (tail levels)@@ -495,14 +493,14 @@ -- RECIPE LEVELS -- ---------------------------------------------------------------------------- --- | Order entites in sets depending on how soon it is possible to obtain them.+-- | Order entities in sets depending on how soon it is possible to obtain them. -- -- So:--- * Level 0 - starting entites (for example those obtainable in the world)+-- * Level 0 - starting entities (for example those obtainable in the world) -- * Level N+1 - everything possible to make (or drill) from Level N ----- This is almost a BFS, but the requirement is that the set of entites--- required for recipe is subset of the entites known in Level N.+-- This is almost a BFS, but the requirement is that the set of entities+-- required for recipe is subset of the entities known in Level N. -- -- If we ever depend on some graph library, this could be rewritten -- as some BFS-like algorithm with added recipe nodes, but you would@@ -524,21 +522,21 @@ then ls else go (n : ls) (Set.union n known) --- | Get classic scenario to figure out starting entites.+-- | Get classic scenario to figure out starting entities. classicScenario :: ExceptT Text IO Scenario classicScenario = do entities <- loadEntities >>= guardRight "load entities" fst <$> loadScenario "data/scenarios/classic.yaml" entities startingDevices :: Scenario -> Set Entity-startingDevices = Set.fromList . map snd . E.elems . view installedDevices . instantiateRobot 0 . head . view scenarioRobots+startingDevices = Set.fromList . map snd . E.elems . view equippedDevices . instantiateRobot 0 . head . view scenarioRobots startingInventory :: Scenario -> Map Entity Int startingInventory = Map.fromList . map swap . E.elems . view robotInventory . instantiateRobot 0 . head . view scenarioRobots --- | Ignore utility entites that are just used for tutorials and challenges.-ignoredEntites :: Set Text-ignoredEntites =+-- | Ignore utility entities that are just used for tutorials and challenges.+ignoredEntities :: Set Text+ignoredEntities = Set.fromList [ "upper left corner" , "upper right corner"
src/Swarm/Game/CESK.hs view
@@ -1,5 +1,5 @@-{-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE PatternSynonyms #-} -- |@@ -64,7 +64,7 @@ -- * Store Store,- Loc,+ Addr, emptyStore, Cell (..), allocate,@@ -82,31 +82,26 @@ -- ** Extracting information finalValue,-- -- ** Pretty-printing- prettyFrame,- prettyCont,- prettyCESK, ) where +import Control.Lens ((^.)) import Control.Lens.Combinators (pattern Empty) import Data.Aeson (FromJSON, ToJSON)-import Data.Aeson qualified import Data.IntMap.Strict (IntMap) import Data.IntMap.Strict qualified as IM-import Data.List (intercalate) import GHC.Generics (Generic)-import Swarm.Game.Entity (Entity, Inventory)+import Prettyprinter (Doc, Pretty (..), hsep, (<+>))+import Swarm.Game.Entity (Count, Entity) import Swarm.Game.Exception-import Swarm.Game.Value as V-import Swarm.Game.World (World)+import Swarm.Game.World (WorldUpdate (..)) import Swarm.Language.Context+import Swarm.Language.Module import Swarm.Language.Pipeline import Swarm.Language.Pretty import Swarm.Language.Requirement (ReqCtx) import Swarm.Language.Syntax import Swarm.Language.Types-import Witch (from)+import Swarm.Language.Value as V ------------------------------------------------------------ -- Frames and continuations@@ -163,12 +158,21 @@ -- in the given environment (extended by binding the variable, -- if there is one, to the output of the first command). FBind (Maybe Var) Term Env+ | -- | Discard any environment generated as the result of executing+ -- a command.+ FDiscardEnv | -- | Apply specific updates to the world and current robot.- FImmediate WorldUpdate RobotUpdate+ --+ -- The 'Const' is used to track the original command for error messages.+ FImmediate Const [WorldUpdate Entity] [RobotUpdate] | -- | Update the memory cell at a certain location with the computed value.- FUpdate Loc+ FUpdate Addr | -- | Signal that we are done with an atomic computation. FFinishAtomic+ | -- | We are in the middle of running a computation for all the+ -- nearby robots. We have the function to run, and the list of+ -- robot IDs to run it on.+ FMeetAll Value [Int] deriving (Eq, Show, Generic, FromJSON, ToJSON) -- | A continuation is just a stack of frames.@@ -178,10 +182,10 @@ -- Store ------------------------------------------------------------ -type Loc = Int+type Addr = Int -- | 'Store' represents a store, indexing integer locations to 'Cell's.-data Store = Store {next :: Loc, mu :: IntMap Cell} deriving (Show, Eq, Generic, FromJSON, ToJSON)+data Store = Store {next :: Addr, mu :: IntMap Cell} deriving (Show, Eq, Generic, FromJSON, ToJSON) -- | A memory cell can be in one of three states. data Cell@@ -213,15 +217,15 @@ -- | Allocate a new memory cell containing an unevaluated expression -- with the current environment. Return the index of the allocated -- cell.-allocate :: Env -> Term -> Store -> (Loc, Store)+allocate :: Env -> Term -> Store -> (Addr, Store) allocate e t (Store n m) = (n, Store (n + 1) (IM.insert n (E t e) m)) -- | Look up the cell at a given index.-lookupCell :: Loc -> Store -> Maybe Cell+lookupCell :: Addr -> Store -> Maybe Cell lookupCell n = IM.lookup n . mu -- | Set the cell at a given index.-setCell :: Loc -> Cell -> Store -> Store+setCell :: Addr -> Cell -> Store -> Store setCell n c (Store nxt m) = Store nxt (IM.insert n c m) ------------------------------------------------------------@@ -281,11 +285,25 @@ -- | Like 'initMachine', but also take an explicit starting continuation. initMachine' :: ProcessedTerm -> Env -> Store -> Cont -> CESK-initMachine' (ProcessedTerm t (Module (Forall _ (TyCmd _)) ctx) _ reqCtx) e s k =- case ctx of- Empty -> In t e s (FExec : k)- _ -> In t e s (FExec : FLoadEnv ctx reqCtx : k)-initMachine' (ProcessedTerm t _ _ _) e s k = In t e s k+initMachine' (ProcessedTerm (Module t' ctx) _ reqCtx) e s k =+ case t' ^. sType of+ -- If the starting term has a command type...+ Forall _ (TyCmd _) ->+ case ctx of+ -- ...but doesn't contain any definitions, just create a machine+ -- that will evaluate it and then execute it.+ Empty -> In t e s (FExec : k)+ -- Or, if it does contain definitions, then load the resulting+ -- context after executing it.+ _ -> In t e s (FExec : FLoadEnv ctx reqCtx : k)+ -- Otherwise, for a term with a non-command type, just+ -- create a machine to evaluate it.+ _ -> In t e s k+ where+ -- Erase all type and SrcLoc annotations from the term before+ -- putting it in the machine state, since those are irrelevant at+ -- runtime.+ t = eraseS t' -- | Cancel the currently running computation. cancel :: CESK -> CESK@@ -307,82 +325,73 @@ resetBlackhole c = c --------------------------------------------------------------- Very crude pretty-printing of CESK states. Should really make a--- nicer version of this code...+-- Pretty printing CESK machine states ------------------------------------------------------------ --- | Very poor pretty-printing of CESK machine states, really just for--- debugging. At some point we should make a nicer version.-prettyCESK :: CESK -> String-prettyCESK (In c _ _ k) =- unlines- [ "▶ " ++ prettyString c- , " " ++ prettyCont k- ]-prettyCESK (Out v _ k) =- unlines- [ "◀ " ++ from (prettyValue v)- , " " ++ prettyCont k- ]-prettyCESK (Up e _ k) =- unlines- [ "! " ++ from (formatExn mempty e)- , " " ++ prettyCont k- ]-prettyCESK (Waiting t cek) =- "🕑" <> show t <> " " <> show cek---- | Poor pretty-printing of continuations.-prettyCont :: Cont -> String-prettyCont = ("[" ++) . (++ "]") . intercalate " | " . map prettyFrame---- | Poor pretty-printing of frames.-prettyFrame :: Frame -> String-prettyFrame (FSnd t _) = "(_, " ++ prettyString t ++ ")"-prettyFrame (FFst v) = "(" ++ from (prettyValue v) ++ ", _)"-prettyFrame (FArg t _) = "_ " ++ prettyString t-prettyFrame (FApp v) = prettyString (valueToTerm v) ++ " _"-prettyFrame (FLet x t _) = "let " ++ from x ++ " = _ in " ++ prettyString t-prettyFrame (FTry t) = "try _ (" ++ prettyString (valueToTerm t) ++ ")"-prettyFrame FUnionEnv {} = "_ ∪ <Env>"-prettyFrame FLoadEnv {} = "loadEnv"-prettyFrame (FDef x) = "def " ++ from x ++ " = _"-prettyFrame FExec = "exec _"-prettyFrame (FBind Nothing t _) = "_ ; " ++ prettyString t-prettyFrame (FBind (Just x) t _) = from x ++ " <- _ ; " ++ prettyString t-prettyFrame FImmediate {} = "(_ : cmd a)"-prettyFrame (FUpdate loc) = "store@" ++ show loc ++ "(_)"-prettyFrame FFinishAtomic = "finishAtomic"+instance PrettyPrec CESK where+ prettyPrec _ (In c _ _ k) = prettyCont k (11, "▶" <> ppr c <> "◀")+ prettyPrec _ (Out v _ k) = prettyCont k (11, "◀" <> ppr (valueToTerm v) <> "▶")+ prettyPrec _ (Up e _ k) = prettyCont k (11, "!" <> (pretty (formatExn mempty e) <> "!"))+ prettyPrec _ (Waiting t cesk) = "🕑" <> pretty t <> "(" <> ppr cesk <> ")" ------------------------------------------------------------------ Wrappers for functions in FImmediate------ NOTE: we can not use GameState and Robot directly, as it--- would create a cyclic dependency. The alternative is--- making CESK, Cont and Frame polymorphic which just muddies--- the picture too much for one little game feature.+-- | Take a continuation, and the pretty-printed expression which is+-- the focus of the continuation (i.e. the expression whose value+-- will be given to the continuation) along with its top-level+-- precedence, and pretty-print the whole thing. ----- BEWARE: the types do not follow normal laws for Show and Eq-----------------------------------------------------------------newtype WorldUpdate = WorldUpdate- { worldUpdate :: World Int Entity -> Either Exn (World Int Entity)- }--newtype RobotUpdate = RobotUpdate- { robotUpdateInventory :: Inventory -> Inventory- }--instance Show WorldUpdate where show _ = "WorldUpdate {???}"+-- As much as possible, we try to print to look like an *expression*+-- with a currently focused part, that is, we print the continuation+-- from the inside out instead of as a list of frames. This makes+-- it much more intuitive to read.+prettyCont :: Cont -> (Int, Doc ann) -> Doc ann+prettyCont [] (_, inner) = inner+prettyCont (f : k) inner = prettyCont k (prettyFrame f inner) -instance Show RobotUpdate where show _ = "RobotUpdate {???}"+-- | Pretty-print a single continuation frame, given its already+-- pretty-printed focus. In particular, given a frame and its+-- "inside" (i.e. the expression or other frames being focused on,+-- whose value will eventually be passed to this frame), with the+-- precedence of the inside's top-level construct, return a+-- pretty-printed version of the entire frame along with its+-- top-level precedence.+prettyFrame :: Frame -> (Int, Doc ann) -> (Int, Doc ann)+prettyFrame (FSnd t _) (_, inner) = (11, "(" <> inner <> "," <+> ppr t <> ")")+prettyFrame (FFst v) (_, inner) = (11, "(" <> ppr (valueToTerm v) <> "," <+> inner <> ")")+prettyFrame (FArg t _) (p, inner) = (10, pparens (p < 10) inner <+> prettyPrec 11 t)+prettyFrame (FApp v) (p, inner) = (10, prettyPrec 10 (valueToTerm v) <+> pparens (p < 11) inner)+prettyFrame (FLet x t _) (_, inner) = (11, hsep ["let", pretty x, "=", inner, "in", ppr t])+prettyFrame (FTry v) (p, inner) = (10, "try" <+> pparens (p < 11) inner <+> prettyPrec 11 (valueToTerm v))+prettyFrame (FUnionEnv _) inner = prettyPrefix "∪·" inner+prettyFrame (FLoadEnv _ _) inner = prettyPrefix "L·" inner+prettyFrame (FDef x) (_, inner) = (11, "def" <+> pretty x <+> "=" <+> inner <+> "end")+prettyFrame FExec inner = prettyPrefix "E·" inner+prettyFrame (FBind Nothing t _) (p, inner) = (0, pparens (p < 1) inner <+> ";" <+> ppr t)+prettyFrame (FBind (Just x) t _) (p, inner) = (0, hsep [pretty x, "<-", pparens (p < 1) inner, ";", ppr t])+prettyFrame FDiscardEnv inner = prettyPrefix "D·" inner+prettyFrame (FImmediate c _worldUpds _robotUpds) inner = prettyPrefix ("I[" <> ppr c <> "]·") inner+prettyFrame (FUpdate addr) inner = prettyPrefix ("S@" <> pretty addr) inner+prettyFrame FFinishAtomic inner = prettyPrefix "A·" inner+prettyFrame (FMeetAll _ _) inner = prettyPrefix "M·" inner -instance Eq WorldUpdate where _ == _ = True+-- | Pretty-print a special "prefix application" frame, i.e. a frame+-- formatted like @X· inner@. Unlike typical applications, these+-- associate to the *right*, so that we can print something like @X·+-- Y· Z· inner@ with no parens.+prettyPrefix :: Doc ann -> (Int, Doc ann) -> (Int, Doc ann)+prettyPrefix pre (p, inner) = (11, pre <+> pparens (p < 11) inner) -instance Eq RobotUpdate where _ == _ = True+--------------------------------------------------------------+-- Runtime robot update+-------------------------------------------------------------- --- TODO: remove these instances once Update fields are concret-instance FromJSON WorldUpdate where parseJSON _ = pure $ WorldUpdate $ \w -> Right w-instance ToJSON WorldUpdate where toJSON _ = Data.Aeson.Null-instance FromJSON RobotUpdate where parseJSON _ = pure $ RobotUpdate id-instance ToJSON RobotUpdate where toJSON _ = Data.Aeson.Null+-- | Update the robot in an inspectable way.+--+-- This type is used for changes by e.g. the drill command at later+-- tick. Using ADT allows us to serialize and inspect the updates.+--+-- Note that this can not be in 'Swarm.Game.Robot' as it would create+-- a cyclic dependency.+data RobotUpdate+ = AddEntity Count Entity+ | LearnEntity Entity+ deriving (Eq, Ord, Show, Generic, FromJSON, ToJSON)
src/Swarm/Game/Display.hs view
@@ -44,9 +44,9 @@ import Data.Maybe (fromMaybe, isJust) import Data.Yaml import GHC.Generics (Generic)-import Swarm.Language.Syntax (Direction (..))+import Swarm.Language.Syntax (AbsoluteDir (..), Direction (..)) import Swarm.TUI.Attr (entityAttr, robotAttr, worldPrefix)-import Swarm.Util (maxOn, (?))+import Swarm.Util (maxOn) import Swarm.Util.Yaml (FromJSONE (..), With (runE), getE, liftE, withObjectE) -- | Display priority. Entities with higher priority will be drawn on@@ -60,7 +60,7 @@ -- | A record explaining how to display an entity in the TUI. data Display = Display { _defaultChar :: Char- , _orientationMap :: Map Direction Char+ , _orientationMap :: Map AbsoluteDir Char , _curOrientation :: Maybe Direction , _displayAttr :: AttrName , _displayPriority :: Priority@@ -83,7 +83,7 @@ -- optionally associates different display characters with -- different orientations. If an orientation is not in the map, -- the 'defaultChar' will be used.-orientationMap :: Lens' Display (Map Direction Char)+orientationMap :: Lens' Display (Map AbsoluteDir Char) -- | The display caches the current orientation of the entity, so we -- know which character to use from the orientation map.@@ -128,9 +128,9 @@ -- | Look up the character that should be used for a display. displayChar :: Display -> Char-displayChar disp = case disp ^. curOrientation of- Nothing -> disp ^. defaultChar- Just dir -> M.lookup dir (disp ^. orientationMap) ? (disp ^. defaultChar)+displayChar disp = fromMaybe (disp ^. defaultChar) $ do+ DAbsolute d <- disp ^. curOrientation+ M.lookup d (disp ^. orientationMap) -- | Render a display as a UI widget. renderDisplay :: Display -> Widget n@@ -167,7 +167,7 @@ -- priority 10. -- -- Note that the 'defaultChar' is used for direction 'DDown'--- and is overriden for the special base robot.+-- and is overridden for the special base robot. defaultRobotDisplay :: Display defaultRobotDisplay = Display
src/Swarm/Game/Entity.hs view
@@ -69,6 +69,8 @@ isSubsetOf, isEmpty, inventoryCapabilities,+ extantElemsWithCapability,+ entitiesByCapability, -- ** Modification insert,@@ -81,32 +83,31 @@ ) where import Control.Arrow ((&&&))-import Control.Lens (Getter, Lens', lens, to, view, (^.), _2)+import Control.Lens (Getter, Lens', lens, to, view, (^.)) import Control.Monad.IO.Class import Data.Bifunctor (bimap, first) import Data.Char (toLower) import Data.Function (on) import Data.Hashable-import Data.Int (Int64) import Data.IntMap (IntMap) import Data.IntMap qualified as IM import Data.IntSet (IntSet) import Data.IntSet qualified as IS import Data.List (foldl')+import Data.List.NonEmpty qualified as NE import Data.Map (Map) import Data.Map qualified as M import Data.Maybe (fromMaybe, isJust, listToMaybe) import Data.Set (Set)-import Data.Set qualified as Set (fromList)-import Data.Set.Lens (setOf)+import Data.Set qualified as Set (fromList, member, toList, unions) import Data.Text (Text) import Data.Text qualified as T import Data.Yaml import GHC.Generics (Generic)-import Linear (V2) import Swarm.Game.Display import Swarm.Language.Capability-import Swarm.Util (dataNotFound, getDataFileNameSafe, plural, reflow, (?))+import Swarm.Util (binTuples, dataNotFound, getDataFileNameSafe, plural, reflow, (?))+import Swarm.Util.Location import Swarm.Util.Yaml import Text.Read (readMaybe) import Witch@@ -198,33 +199,33 @@ -- entities stored in the world that are the same will literally -- just be stored as pointers to the same shared record. data Entity = Entity- { -- | A hash value computed from the other fields- _entityHash :: Int- , -- | The way this entity should be displayed on the world map.- _entityDisplay :: Display- , -- | The name of the entity, used /e.g./ in an inventory display.- _entityName :: Text- , -- | The plural of the entity name, in case it is irregular. If- -- this field is @Nothing@, default pluralization heuristics- -- will be used (see 'plural').- _entityPlural :: Maybe Text- , -- | A longer-form description. Each 'Text' value is one- -- paragraph.- _entityDescription :: [Text]- , -- | The entity's orientation (if it has one). For example, when- -- a robot moves, it moves in the direction of its orientation.- _entityOrientation :: Maybe (V2 Int64)- , -- | If this entity grows, how long does it take?- _entityGrowth :: Maybe GrowthTime- , -- | The name of a different entity obtained when this entity is- -- grabbed.- _entityYields :: Maybe Text- , -- | Properties of the entity.- _entityProperties :: Set EntityProperty- , -- | Capabilities provided by this entity.- _entityCapabilities :: [Capability]- , -- | Inventory of other entities held by this entity.- _entityInventory :: Inventory+ { _entityHash :: Int+ -- ^ A hash value computed from the other fields+ , _entityDisplay :: Display+ -- ^ The way this entity should be displayed on the world map.+ , _entityName :: Text+ -- ^ The name of the entity, used /e.g./ in an inventory display.+ , _entityPlural :: Maybe Text+ -- ^ The plural of the entity name, in case it is irregular. If+ -- this field is @Nothing@, default pluralization heuristics+ -- will be used (see 'plural').+ , _entityDescription :: [Text]+ -- ^ A longer-form description. Each 'Text' value is one+ -- paragraph.+ , _entityOrientation :: Maybe Heading+ -- ^ The entity's orientation (if it has one). For example, when+ -- a robot moves, it moves in the direction of its orientation.+ , _entityGrowth :: Maybe GrowthTime+ -- ^ If this entity grows, how long does it take?+ , _entityYields :: Maybe Text+ -- ^ The name of a different entity obtained when this entity is+ -- grabbed.+ , _entityProperties :: Set EntityProperty+ -- ^ Properties of the entity.+ , _entityCapabilities :: Set Capability+ -- ^ Capabilities provided by this entity.+ , _entityInventory :: Inventory+ -- ^ Inventory of other entities held by this entity. } -- Note that an entity does not have a location, because the -- location of an entity is implicit in the way it is stored (by@@ -236,7 +237,8 @@ -- value and simply combines the other fields. instance Hashable Entity where hashWithSalt s (Entity _ disp nm pl descr orient grow yld props caps inv) =- s `hashWithSalt` disp+ s+ `hashWithSalt` disp `hashWithSalt` nm `hashWithSalt` pl `hashWithSalt` descr@@ -275,7 +277,7 @@ [Capability] -> Entity mkEntity disp nm descr props caps =- rehashEntity $ Entity 0 disp nm Nothing descr Nothing Nothing Nothing (Set.fromList props) caps empty+ rehashEntity $ Entity 0 disp nm Nothing descr Nothing Nothing Nothing (Set.fromList props) (Set.fromList caps) empty ------------------------------------------------------------ -- Entity map@@ -291,7 +293,7 @@ deriving (Eq, Show, Generic, FromJSON, ToJSON) instance Semigroup EntityMap where- EntityMap n1 c1 <> EntityMap n2 c2 = EntityMap (n1 <> n2) (c1 <> c2)+ EntityMap n1 c1 <> EntityMap n2 c2 = EntityMap (n1 <> n2) (M.unionWith (<>) c1 c2) instance Monoid EntityMap where mempty = EntityMap M.empty M.empty@@ -313,7 +315,7 @@ buildEntityMap es = EntityMap { entitiesByName = M.fromList . map (view entityName &&& id) $ es- , entitiesByCap = M.fromListWith (<>) . concatMap (\e -> map (,[e]) (e ^. entityCapabilities)) $ es+ , entitiesByCap = M.fromListWith (<>) . concatMap (\e -> map (,[e]) (Set.toList $ e ^. entityCapabilities)) $ es } ------------------------------------------------------------@@ -332,7 +334,7 @@ <*> v .:? "growth" <*> v .:? "yields" <*> v .:? "properties" .!= mempty- <*> v .:? "capabilities" .!= []+ <*> v .:? "capabilities" .!= mempty <*> pure empty ) @@ -419,7 +421,7 @@ entityDescription = hashedLens _entityDescription (\e x -> e {_entityDescription = x}) -- | The direction this entity is facing (if it has one).-entityOrientation :: Lens' Entity (Maybe (V2 Int64))+entityOrientation :: Lens' Entity (Maybe Heading) entityOrientation = hashedLens _entityOrientation (\e x -> e {_entityOrientation = x}) -- | How long this entity takes to grow, if it regrows.@@ -439,8 +441,8 @@ hasProperty :: Entity -> EntityProperty -> Bool hasProperty e p = p `elem` (e ^. entityProperties) --- | The capabilities this entity provides when installed.-entityCapabilities :: Lens' Entity [Capability]+-- | The capabilities this entity provides when equipped.+entityCapabilities :: Lens' Entity (Set Capability) entityCapabilities = hashedLens _entityCapabilities (\e x -> e {_entityCapabilities = x}) -- | The inventory of other entities carried by this entity.@@ -567,7 +569,25 @@ -- | Compute the set of capabilities provided by the devices in an inventory. inventoryCapabilities :: Inventory -> Set Capability-inventoryCapabilities = setOf (to elems . traverse . _2 . entityCapabilities . traverse)+inventoryCapabilities = Set.unions . map (^. entityCapabilities) . nonzeroEntities++-- | List elements that have at least one copy in the inventory.+nonzeroEntities :: Inventory -> [Entity]+nonzeroEntities = map snd . filter ((> 0) . fst) . elems++-- | List elements that possess a given Capability and+-- exist with nonzero count in the inventory.+extantElemsWithCapability :: Capability -> Inventory -> [Entity]+extantElemsWithCapability cap =+ filter (Set.member cap . (^. entityCapabilities)) . nonzeroEntities++-- | Groups entities by the capabilities they offer.+entitiesByCapability :: Inventory -> Map Capability (NE.NonEmpty Entity)+entitiesByCapability inv =+ binTuples entityCapabilityPairs+ where+ getCaps = Set.toList . (^. entityCapabilities)+ entityCapabilityPairs = concatMap ((\e -> map (,e) $ getCaps e) . snd) $ elems inv -- | Delete a single copy of a certain entity from an inventory. delete :: Entity -> Inventory -> Inventory
src/Swarm/Game/Exception.hs view
@@ -1,4 +1,3 @@-{-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} @@ -32,6 +31,7 @@ import Swarm.Language.Pretty (prettyText) import Swarm.Language.Requirement (Requirements (..)) import Swarm.Language.Syntax (Const, Term)+import Swarm.TUI.Model.Achievement.Definitions import Swarm.Util import Witch (from) @@ -52,8 +52,8 @@ -- | Suggested way to fix incapable error. data IncapableFix- = -- | Install the missing device on yourself/target- FixByInstall+ = -- | Equip the missing device on yourself/target+ FixByEquip | -- | Add the missing device to your inventory FixByObtain deriving (Eq, Show, Generic, FromJSON, ToJSON)@@ -74,7 +74,7 @@ | -- | A command failed in some "normal" way (/e.g./ a 'Move' -- command could not move, or a 'Grab' command found nothing to -- grab, /etc./).- CmdFailed Const Text+ CmdFailed Const Text (Maybe GameplayAchievement) | -- | The user program explicitly called 'Undefined' or 'Fail'. User Text deriving (Eq, Show, Generic, FromJSON, ToJSON)@@ -89,7 +89,7 @@ , "<https://github.com/swarm-game/swarm/issues/new>." ] InfiniteLoop -> "Infinite loop detected!"- (CmdFailed c t) -> T.concat [prettyText c, ": ", t]+ (CmdFailed c t _) -> T.concat [prettyText c, ": ", t] (User t) -> "Player exception: " <> t (Incapable f caps tm) -> formatIncapable em f caps tm @@ -99,7 +99,7 @@ formatIncapableFix :: IncapableFix -> Text formatIncapableFix = \case- FixByInstall -> "install"+ FixByEquip -> "equip" FixByObtain -> "obtain" -- | Pretty print the incapable exception with an actionable suggestion@@ -108,7 +108,7 @@ -- >>> w = mkEntity (defaultEntityDisplay 'l') "magic wand" [] [] [CAppear] -- >>> r = mkEntity (defaultEntityDisplay 'o') "the one ring" [] [] [CAppear] -- >>> m = buildEntityMap [w,r]--- >>> incapableError cs t = putStr . unpack $ formatIncapable m FixByInstall cs t+-- >>> incapableError cs t = putStr . unpack $ formatIncapable m FixByEquip cs t -- -- >>> incapableError (R.singletonCap CGod) (TConst As) -- Thou shalt not utter such blasphemy:@@ -118,7 +118,7 @@ -- >>> incapableError (R.singletonCap CAppear) (TConst Appear) -- You do not have the devices required for: -- 'appear'--- Please install:+-- Please equip: -- - the one ring or magic wand -- -- >>> incapableError (R.singletonCap CRandom) (TConst Random)@@ -135,32 +135,32 @@ formatIncapable :: EntityMap -> IncapableFix -> Requirements -> Term -> Text formatIncapable em f (Requirements caps _ inv) tm | CGod `S.member` caps =- unlinesExText- [ "Thou shalt not utter such blasphemy:"- , squote $ prettyText tm- , "If God in troth thou wantest to play, try thou a Creative game."- ]+ unlinesExText+ [ "Thou shalt not utter such blasphemy:"+ , squote $ prettyText tm+ , "If God in troth thou wantest to play, try thou a Creative game."+ ] | not (null capsNone) =- unlinesExText- [ "Missing the " <> capMsg <> " for:"- , squote $ prettyText tm- , "but no device yet provides it. See"- , "https://github.com/swarm-game/swarm/issues/26"- ]+ unlinesExText+ [ "Missing the " <> capMsg <> " for:"+ , squote $ prettyText tm+ , "but no device yet provides it. See"+ , "https://github.com/swarm-game/swarm/issues/26"+ ] | not (S.null caps) =- unlinesExText- ( "You do not have the devices required for:" :- squote (prettyText tm) :- "Please " <> formatIncapableFix f <> ":" :- (("- " <>) . formatDevices <$> filter (not . null) deviceSets)- )+ unlinesExText+ ( "You do not have the devices required for:"+ : squote (prettyText tm)+ : "Please " <> formatIncapableFix f <> ":"+ : (("- " <>) . formatDevices <$> filter (not . null) deviceSets)+ ) | otherwise =- unlinesExText- ( "You are missing required inventory for:" :- squote (prettyText tm) :- "Please obtain:" :- (("- " <>) . formatEntity <$> M.assocs inv)- )+ unlinesExText+ ( "You are missing required inventory for:"+ : squote (prettyText tm)+ : "Please obtain:"+ : (("- " <>) . formatEntity <$> M.assocs inv)+ ) where capList = S.toList caps deviceSets = map (`deviceForCap` em) capList
+ src/Swarm/Game/Log.hs view
@@ -0,0 +1,74 @@+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TemplateHaskell #-}++-- |+-- Module : Swarm.Game.Log+-- Copyright : Ondřej Šebek+-- Maintainer : ondras98@icloud.com+--+-- SPDX-License-Identifier: BSD-3-Clause+--+-- A data type to represent in-game logs by robots.+--+-- Because of the use of system robots, we sometimes+-- want to use special kinds of logs that will be+-- shown to the player.+--+-- TODO: #1039 Currently we abuse this system for system+-- logs, which is fun, but we should eventually make+-- a dedicated `SystemLogEntry` type for 'RuntimeState'+-- message queue.+module Swarm.Game.Log (+ LogSource (..),+ ErrorLevel (..),++ -- * Robot log entries+ LogEntry (..),+ leText,+ leSource,+ leRobotName,+ leTime,+ leLocation,+ leRobotID,+) where++import Control.Lens hiding (contains)+import Data.Aeson (FromJSON, ToJSON)+import Data.Text (Text)+import GHC.Generics (Generic)+import Swarm.Util.Location (Location)++-- | Severity of the error - critical errors are bugs+-- and should be reported as Issues.+data ErrorLevel = Debug | Warning | Error | Critical+ deriving (Show, Eq, Ord, Generic, FromJSON, ToJSON)++-- | Source of the robot log.+data LogSource+ = -- | Log produced by 'Swarm.Language.Syntax.Say'+ Said+ | -- | Log produced by 'Swarm.Language.Syntax.Log'+ Logged+ | -- | Log produced by an exception or system.+ ErrorTrace ErrorLevel+ deriving (Show, Eq, Ord, Generic, FromJSON, ToJSON)++-- | An entry in a robot's log.+data LogEntry = LogEntry+ { _leTime :: Integer+ -- ^ The time at which the entry was created.+ -- Note that this is the first field we sort on.+ , _leSource :: LogSource+ -- ^ Whether this log records a said message.+ , _leRobotName :: Text+ -- ^ The name of the robot that generated the entry.+ , _leRobotID :: Int+ -- ^ The ID of the robot that generated the entry.+ , _leLocation :: Location+ -- ^ Location of the robot at log entry creation.+ , _leText :: Text+ -- ^ The text of the log entry.+ }+ deriving (Show, Eq, Ord, Generic, FromJSON, ToJSON)++makeLenses ''LogEntry
src/Swarm/Game/Recipe.hs view
@@ -1,4 +1,3 @@-{-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TemplateHaskell #-} @@ -191,7 +190,7 @@ -- | Figure out which ingredients (if any) are lacking from an -- inventory to be able to carry out the recipe.--- Requirements are not consumed and so can use installed.+-- Requirements are not consumed and so can use equipped. missingIngredientsFor :: (Inventory, Inventory) -> Recipe Entity -> [MissingIngredient] missingIngredientsFor (inv, ins) (Recipe inps _ reqs _ _) = mkMissing MissingInput (findLacking inv inps)@@ -214,7 +213,7 @@ -- or an inventory without inputs and function adding outputs if -- it was successful. make ::- -- robots inventory and installed devices+ -- robots inventory and equipped devices (Inventory, Inventory) -> -- considered recipe Recipe Entity ->@@ -222,11 +221,10 @@ -- a function to add results and the recipe repeated Either [MissingIngredient]- (Inventory, Inventory -> Inventory, Recipe Entity)+ (Inventory, IngredientList Entity, Recipe Entity) make invs r = finish <$> make' invs r where- finish (invTaken, out) = (invTaken, addOuts out, r)- addOuts out inv' = foldl' (flip $ uncurry insertCount) inv' out+ finish (invTaken, out) = (invTaken, out, r) -- | Try to make a recipe, but do not insert it yet. make' :: (Inventory, Inventory) -> Recipe Entity -> Either [MissingIngredient] (Inventory, IngredientList Entity)
src/Swarm/Game/Robot.hs view
@@ -1,5 +1,4 @@ {-# LANGUAGE DataKinds #-}-{-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE TemplateHaskell #-}@@ -19,14 +18,7 @@ -- * Robots data -- * Robot log entries- LogSource (..),- LogEntry (..),- leText,- leSaid,- leRobotName,- leTime,- leLocation,- leRobotID,+ module Swarm.Game.Log, -- * Robots RobotPhase (..),@@ -35,6 +27,9 @@ Robot, TRobot, + -- ** Runtime robot update+ RobotUpdate (..),+ -- * Robot context RobotContext, defTypes,@@ -54,12 +49,13 @@ trobotLocation, robotOrientation, robotInventory,- installedDevices,+ equippedDevices, robotLog, robotLogUpdated, inventoryHash, robotCapabilities, robotContext,+ trobotContext, robotID, robotParentID, robotHeavy,@@ -86,7 +82,6 @@ import Control.Lens hiding (contains) import Data.Aeson (FromJSON, ToJSON) import Data.Hashable (hashWithSalt)-import Data.Int (Int64) import Data.Maybe (fromMaybe, isNothing) import Data.Sequence (Seq) import Data.Sequence qualified as Seq@@ -98,32 +93,33 @@ import Swarm.Game.CESK import Swarm.Game.Display (Display, curOrientation, defaultRobotDisplay, invisible) import Swarm.Game.Entity hiding (empty)-import Swarm.Game.Value as V+import Swarm.Game.Log import Swarm.Language.Capability (Capability) import Swarm.Language.Context qualified as Ctx import Swarm.Language.Requirement (ReqCtx) import Swarm.Language.Syntax (toDirection) import Swarm.Language.Typed (Typed (..)) import Swarm.Language.Types (TCtx)-import Swarm.Util ()+import Swarm.Language.Value as V+import Swarm.Util.Location import Swarm.Util.Yaml import System.Clock (TimeSpec) -- | A record that stores the information--- for all defintions stored in a 'Robot'+-- for all definitions stored in a 'Robot' data RobotContext = RobotContext- { -- | Map definition names to their types.- _defTypes :: TCtx- , -- | Map defintion names to the capabilities- -- required to evaluate/execute them.- _defReqs :: ReqCtx- , -- | Map defintion names to their values. Note that since- -- definitions are delayed, the values will just consist of- -- 'VRef's pointing into the store.- _defVals :: Env- , -- | A store containing memory cells allocated to hold- -- definitions.- _defStore :: Store+ { _defTypes :: TCtx+ -- ^ Map definition names to their types.+ , _defReqs :: ReqCtx+ -- ^ Map definition names to the capabilities+ -- required to evaluate/execute them.+ , _defVals :: Env+ -- ^ Map definition names to their values. Note that since+ -- definitions are delayed, the values will just consist of+ -- 'VRef's pointing into the store.+ , _defStore :: Store+ -- ^ A store containing memory cells allocated to hold+ -- definitions. } deriving (Eq, Show, Generic, FromJSON, ToJSON) @@ -146,37 +142,16 @@ req <- Ctx.lookup name (ctx ^. defReqs) return $ Typed val typ req setter ctx Nothing =- ctx & defTypes %~ Ctx.delete name+ ctx+ & defTypes %~ Ctx.delete name & defVals %~ Ctx.delete name & defReqs %~ Ctx.delete name setter ctx (Just (Typed val typ req)) =- ctx & defTypes %~ Ctx.addBinding name typ+ ctx+ & defTypes %~ Ctx.addBinding name typ & defVals %~ Ctx.addBinding name val & defReqs %~ Ctx.addBinding name req -data LogSource = Said | Logged | ErrorTrace- deriving (Show, Eq, Ord, Generic, FromJSON, ToJSON)---- | An entry in a robot's log.-data LogEntry = LogEntry- { -- | The time at which the entry was created.- -- Note that this is the first field we sort on.- _leTime :: Integer- , -- | Whether this log records a said message.- _leSaid :: LogSource- , -- | The name of the robot that generated the entry.- _leRobotName :: Text- , -- | The ID of the robot that generated the entry.- _leRobotID :: Int- , -- | Location of the robot at log entry creation.- _leLocation :: V2 Int64- , -- | The text of the log entry.- _leText :: Text- }- deriving (Show, Eq, Ord, Generic, FromJSON, ToJSON)--makeLenses ''LogEntry- -- | A unique identifier for a robot. type RID = Int @@ -192,8 +167,8 @@ -- | With a robot template, we may or may not have a location. With a -- concrete robot we must have a location. type family RobotLocation (phase :: RobotPhase) :: * where- RobotLocation 'TemplateRobot = Maybe (V2 Int64)- RobotLocation 'ConcreteRobot = V2 Int64+ RobotLocation 'TemplateRobot = Maybe Location+ RobotLocation 'ConcreteRobot = Location -- | Robot templates have no ID; concrete robots definitely do. type family RobotID (phase :: RobotPhase) :: * where@@ -205,10 +180,10 @@ -- the robot has been assigned a unique ID. data RobotR (phase :: RobotPhase) = RobotR { _robotEntity :: Entity- , _installedDevices :: Inventory- , -- | A cached view of the capabilities this robot has.- -- Automatically generated from '_installedDevices'.- _robotCapabilities :: Set Capability+ , _equippedDevices :: Inventory+ , _robotCapabilities :: Set Capability+ -- ^ A cached view of the capabilities this robot has.+ -- Automatically generated from '_equippedDevices'. , _robotLog :: Seq LogEntry , _robotLogUpdated :: Bool , _robotLocation :: RobotLocation phase@@ -233,7 +208,7 @@ -- See https://byorgey.wordpress.com/2021/09/17/automatically-updated-cached-views-with-lens/ -- for the approach used here with lenses. -let exclude = ['_robotCapabilities, '_installedDevices, '_robotLog]+let exclude = ['_robotCapabilities, '_equippedDevices, '_robotLog] in makeLensesWith ( lensRules & generateSignatures .~ False@@ -299,32 +274,36 @@ -- a getter, since when changing a robot's location we must remember -- to update the 'robotsByLocation' map as well. You can use the -- 'updateRobotLocation' function for this purpose.-robotLocation :: Getter Robot (V2 Int64)+robotLocation :: Getter Robot Location -- | Set a robot's location. This is unsafe and should never be -- called directly except by the 'updateRobotLocation' function. -- The reason is that we need to make sure the 'robotsByLocation' -- map stays in sync.-unsafeSetRobotLocation :: V2 Int64 -> Robot -> Robot+unsafeSetRobotLocation :: Location -> Robot -> Robot unsafeSetRobotLocation loc r = r {_robotLocation = loc} -- | A template robot's location. Unlike 'robotLocation', this is a -- lens, since when dealing with robot templates there is as yet no -- 'robotsByLocation' map to keep up-to-date.-trobotLocation :: Lens' TRobot (Maybe (V2 Int64))+trobotLocation :: Lens' TRobot (Maybe Location) trobotLocation = lens _robotLocation (\r l -> r {_robotLocation = l}) -- | Which way the robot is currently facing.-robotOrientation :: Lens' Robot (Maybe (V2 Int64))+robotOrientation :: Lens' Robot (Maybe Heading) robotOrientation = robotEntity . entityOrientation -- | The robot's inventory. robotInventory :: Lens' Robot Inventory robotInventory = robotEntity . entityInventory --- | The robot's context+-- | The robot's context. robotContext :: Lens' Robot RobotContext +-- | The robot's context.+trobotContext :: Lens' TRobot RobotContext+trobotContext = lens _robotContext (\r c -> r {_robotContext = c})+ -- | The (unique) ID number of the robot. This is only a Getter since -- the robot ID is immutable. robotID :: Getter Robot RID@@ -338,7 +317,7 @@ instantiateRobot i r = r { _robotID = i- , _robotLocation = fromMaybe (V2 0 0) (_robotLocation r)+ , _robotLocation = fromMaybe zero (_robotLocation r) } -- | The ID number of the robot's parent, that is, the robot that@@ -349,19 +328,19 @@ -- | Is this robot extra heavy (thus requiring tank treads to move)? robotHeavy :: Lens' Robot Bool --- | A separate inventory for "installed devices", which provide the+-- | A separate inventory for equipped devices, which provide the -- robot with certain capabilities. ----- Note that every time the inventory of installed devices is+-- Note that every time the inventory of equipped devices is -- modified, this lens recomputes a cached set of the capabilities--- the installed devices provide, to speed up subsequent lookups to+-- the equipped devices provide, to speed up subsequent lookups to -- see whether the robot has a certain capability (see 'robotCapabilities')-installedDevices :: Lens' Robot Inventory-installedDevices = lens _installedDevices setInstalled+equippedDevices :: Lens' Robot Inventory+equippedDevices = lens _equippedDevices setEquipped where- setInstalled r inst =+ setEquipped r inst = r- { _installedDevices = inst+ { _equippedDevices = inst , _robotCapabilities = inventoryCapabilities inst } @@ -390,20 +369,20 @@ -- viewed? robotLogUpdated :: Lens' Robot Bool --- | A hash of a robot's entity record and installed devices, to+-- | A hash of a robot's entity record and equipped devices, to -- facilitate quickly deciding whether we need to redraw the robot -- info panel. inventoryHash :: Getter Robot Int-inventoryHash = to (\r -> 17 `hashWithSalt` (r ^. (robotEntity . entityHash)) `hashWithSalt` (r ^. installedDevices))+inventoryHash = to (\r -> 17 `hashWithSalt` (r ^. (robotEntity . entityHash)) `hashWithSalt` (r ^. equippedDevices)) -- | Does a robot know of an entity's existence? robotKnows :: Robot -> Entity -> Bool-robotKnows r e = contains0plus e (r ^. robotInventory) || contains0plus e (r ^. installedDevices)+robotKnows r e = contains0plus e (r ^. robotInventory) || contains0plus e (r ^. equippedDevices) -- | Get the set of capabilities this robot possesses. This is only a -- getter, not a lens, because it is automatically generated from--- the 'installedDevices'. The only way to change a robot's--- capabilities is to modify its 'installedDevices'.+-- the 'equippedDevices'. The only way to change a robot's+-- capabilities is to modify its 'equippedDevices'. robotCapabilities :: Getter Robot (Set Capability) robotCapabilities = to _robotCapabilities @@ -472,12 +451,12 @@ -- | Initial location. RobotLocation phase -> -- | Initial heading/direction.- V2 Int64 ->+ Heading -> -- | Robot display. Display -> -- | Initial CESK machine. CESK ->- -- | Installed devices.+ -- | Equipped devices. [Entity] -> -- | Initial inventory. [(Count, Entity)] ->@@ -494,7 +473,7 @@ mkEntity disp name descr [] [] & entityOrientation ?~ dir & entityInventory .~ fromElems inv- , _installedDevices = inst+ , _equippedDevices = inst , _robotCapabilities = inventoryCapabilities inst , _robotLog = Seq.empty , _robotLogUpdated = False
src/Swarm/Game/Scenario.hs view
@@ -1,5 +1,4 @@ {-# LANGUAGE DefaultSignatures #-}-{-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE DerivingVia #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-}@@ -17,14 +16,11 @@ -- conditions, which can be used both for building interactive -- tutorials and for standalone puzzles and scenarios. module Swarm.Game.Scenario (- -- * Objectives- Objective,- objectiveGoal,- objectiveCondition,- -- * WorldDescription- Cell (..),- WorldDescription (..),+ PCell (..),+ Cell,+ PWorldDescription (..),+ WorldDescription, IndexedTRobot, -- * Scenario@@ -37,6 +33,7 @@ scenarioDescription, scenarioCreative, scenarioSeed,+ scenarioAttrs, scenarioEntities, scenarioRecipes, scenarioKnown,@@ -56,183 +53,29 @@ import Control.Carrier.Lift (Lift, sendIO) import Control.Carrier.Throw.Either (Throw, throwError) import Control.Lens hiding (from, (<.>))-import Control.Monad (filterM, when)-import Control.Monad.Extra (mapMaybeM)-import Data.Aeson.Key qualified as Key-import Data.Aeson.KeyMap (KeyMap)-import Data.Aeson.KeyMap qualified as KeyMap-import Data.Map (Map)-import Data.Map qualified as M+import Control.Monad (filterM)+import Data.Aeson import Data.Maybe (catMaybes, isNothing, listToMaybe) import Data.Text (Text) import Data.Text qualified as T-import Data.Vector qualified as V import Data.Yaml as Y-import GHC.Generics (Generic)-import GHC.Int (Int64)-import Linear.V2 import Swarm.Game.Entity import Swarm.Game.Recipe-import Swarm.Game.Robot (TRobot, trobotName)-import Swarm.Game.Terrain+import Swarm.Game.Robot (TRobot)+import Swarm.Game.Scenario.Cell+import Swarm.Game.Scenario.Objective+import Swarm.Game.Scenario.Objective.Validation+import Swarm.Game.Scenario.RobotLookup+import Swarm.Game.Scenario.Style+import Swarm.Game.Scenario.WorldDescription import Swarm.Language.Pipeline (ProcessedTerm)-import Swarm.Util (getDataFileNameSafe, reflow)+import Swarm.Util (getDataFileNameSafe) import Swarm.Util.Yaml import System.Directory (doesFileExist) import System.FilePath ((<.>), (</>)) import Witch (from, into) --------------------------------------------------------------- Scenario objectives----------------------------------------------------------------- | An objective is a condition to be achieved by a player in a--- scenario.-data Objective = Objective- { _objectiveGoal :: [Text]- , _objectiveCondition :: ProcessedTerm- }- deriving (Eq, Show, Generic, ToJSON)--makeLensesWith (lensRules & generateSignatures .~ False) ''Objective---- | An explanation of the goal of the objective, shown to the player--- during play. It is represented as a list of paragraphs.-objectiveGoal :: Lens' Objective [Text]---- | A winning condition for the objective, expressed as a--- program of type @cmd bool@. By default, this program will be--- run to completion every tick (the usual limits on the number--- of CESK steps per tick do not apply).-objectiveCondition :: Lens' Objective ProcessedTerm--instance FromJSON Objective where- parseJSON = withObject "objective" $ \v ->- Objective- <$> (fmap . map) reflow (v .:? "goal" .!= [])- <*> (v .: "condition")----------------------------------------------------------------- Robot map----------------------------------------------------------------- | A robot template paired with its definition's index within--- the Scenario file-type IndexedTRobot = (Int, TRobot)---- | A map from names to robots, used to look up robots in scenario--- descriptions.-type RobotMap = Map Text IndexedTRobot---- | Create a 'RobotMap' from a list of robot templates.-buildRobotMap :: [TRobot] -> RobotMap-buildRobotMap rs = M.fromList $ zipWith (\x y -> (view trobotName y, (x, y))) [0 ..] rs----------------------------------------------------------------- Lookup utilities----------------------------------------------------------------- | Look up a thing by name, throwing a parse error if it is not--- found.-getThing :: String -> (Text -> m -> Maybe a) -> Text -> ParserE m a-getThing thing lkup name = do- m <- getE- case lkup name m of- Nothing -> fail $ "Unknown " <> thing <> " name: " ++ show name- Just a -> return a---- | Look up an entity by name in an 'EntityMap', throwing a parse--- error if it is not found.-getEntity :: Text -> ParserE EntityMap Entity-getEntity = getThing "entity" lookupEntityName---- | Look up a robot by name in a 'RobotMap', throwing a parse error--- if it is not found.-getRobot :: Text -> ParserE RobotMap IndexedTRobot-getRobot = getThing "robot" M.lookup----------------------------------------------------------------- World cells----------------------------------------------------------------- | A single cell in a world map, which contains a terrain value,--- and optionally an entity and robot.-data Cell = Cell- { cellTerrain :: TerrainType- , cellEntity :: Maybe Entity- , cellRobots :: [IndexedTRobot]- }- deriving (Eq, Show)---- | Parse a tuple such as @[grass, rock, base]@ into a 'Cell'. The--- entity and robot, if present, are immediately looked up and--- converted into 'Entity' and 'TRobot' values. If they are not--- found, a parse error results.-instance FromJSONE (EntityMap, RobotMap) Cell where- parseJSONE = withArrayE "tuple" $ \v -> do- let tup = V.toList v- when (null tup) $ fail "palette entry must nonzero length (terrain, optional entity and then robots if any)"-- terr <- liftE $ parseJSON (head tup)-- ent <- case tup ^? ix 1 of- Nothing -> return Nothing- Just e -> do- meName <- liftE $ parseJSON @(Maybe Text) e- traverse (localE fst . getEntity) meName-- let name2rob r = do- mrName <- liftE $ parseJSON @(Maybe Text) r- traverse (localE snd . getRobot) mrName-- robs <- mapMaybeM name2rob (drop 2 tup)-- return $ Cell terr ent robs----------------------------------------------------------------- World description----------------------------------------------------------------- | A world palette maps characters to 'Cell' values.-newtype WorldPalette = WorldPalette- {unPalette :: KeyMap Cell}- deriving (Eq, Show)--instance FromJSONE (EntityMap, RobotMap) WorldPalette where- parseJSONE = withObjectE "palette" $ fmap WorldPalette . mapM parseJSONE---- | A description of a world parsed from a YAML file.-data WorldDescription = WorldDescription- { defaultTerrain :: Maybe Cell- , offsetOrigin :: Bool- , palette :: WorldPalette- , ul :: V2 Int64- , area :: [[Cell]]- }- deriving (Eq, Show)--instance FromJSONE (EntityMap, RobotMap) WorldDescription where- parseJSONE = withObjectE "world description" $ \v -> do- pal <- v ..:? "palette" ..!= WorldPalette mempty- WorldDescription- <$> v ..:? "default"- <*> liftE (v .:? "offset" .!= False)- <*> pure pal- <*> liftE (v .:? "upperleft" .!= V2 0 0)- <*> liftE ((v .:? "map" .!= "") >>= paintMap pal)---- | "Paint" a world map using a 'WorldPalette', turning it from a raw--- string into a nested list of 'Cell' values by looking up each--- character in the palette, failing if any character in the raw map--- is not contained in the palette.-paintMap :: MonadFail m => WorldPalette -> Text -> m [[Cell]]-paintMap pal = traverse (traverse toCell . into @String) . T.lines- where- toCell c = case KeyMap.lookup (Key.fromString [c]) (unPalette pal) of- Nothing -> fail $ "Char not in world palette: " ++ show c- Just cell -> return cell-------------------------------------------------------------- -- Scenario ------------------------------------------------------------ @@ -245,6 +88,7 @@ , _scenarioDescription :: Text , _scenarioCreative :: Bool , _scenarioSeed :: Maybe Int+ , _scenarioAttrs :: [CustomAttr] , _scenarioEntities :: EntityMap , _scenarioRecipes :: [Recipe Entity] , _scenarioKnown :: [Text]@@ -263,6 +107,7 @@ -- parse custom entities em <- liftE (buildEntityMap <$> (v .:? "entities" .!= [])) -- extend ambient EntityMap with custom entities+ withE em $ do -- parse 'known' entity names and make sure they exist known <- liftE (v .:? "known" .!= [])@@ -284,12 +129,13 @@ <*> liftE (v .:? "description" .!= "") <*> liftE (v .:? "creative" .!= False) <*> liftE (v .:? "seed")+ <*> liftE (v .:? "attrs" .!= []) <*> pure em <*> v ..:? "recipes" ..!= [] <*> pure known <*> localE (,rsMap) (v ..: "world") <*> pure rs- <*> liftE (v .:? "objectives" .!= [])+ <*> (liftE (v .:? "objectives" .!= []) >>= validateObjectives) <*> liftE (v .:? "solution") <*> liftE (v .:? "stepsPerTick") @@ -318,6 +164,9 @@ -- | The seed used for the random number generator. If @Nothing@, use -- a random seed / prompt the user for the seed. scenarioSeed :: Lens' Scenario (Maybe Int)++-- | Custom attributes defined in the scenario.+scenarioAttrs :: Lens' Scenario [CustomAttr] -- | Any custom entities used for this scenario. scenarioEntities :: Lens' Scenario EntityMap
+ src/Swarm/Game/Scenario/Cell.hs view
@@ -0,0 +1,62 @@+{-# LANGUAGE DerivingVia #-}+{-# LANGUAGE OverloadedStrings #-}++module Swarm.Game.Scenario.Cell (+ PCell (..),+ Cell,+) where++import Control.Lens hiding (from, (<.>))+import Control.Monad (when)+import Control.Monad.Extra (mapMaybeM)+import Data.Text (Text)+import Data.Vector qualified as V+import Data.Yaml as Y+import Swarm.Game.Entity+import Swarm.Game.Scenario.RobotLookup+import Swarm.Game.Terrain+import Swarm.Util.Yaml++------------------------------------------------------------+-- World cells+------------------------------------------------------------++-- | A single cell in a world map, which contains a terrain value,+-- and optionally an entity and robot.+-- It is parameterized on the Entity type to facilitate less+-- stateful versions of the Entity type in rendering scenario data.+data PCell e = Cell+ { cellTerrain :: TerrainType+ , cellEntity :: Maybe e+ , cellRobots :: [IndexedTRobot]+ }+ deriving (Eq, Show)++-- | A single cell in a world map, which contains a terrain value,+-- and optionally an entity and robot.+type Cell = PCell Entity++-- | Parse a tuple such as @[grass, rock, base]@ into a 'Cell'. The+-- entity and robot, if present, are immediately looked up and+-- converted into 'Entity' and 'TRobot' values. If they are not+-- found, a parse error results.+instance FromJSONE (EntityMap, RobotMap) Cell where+ parseJSONE = withArrayE "tuple" $ \v -> do+ let tup = V.toList v+ when (null tup) $ fail "palette entry must nonzero length (terrain, optional entity and then robots if any)"++ terr <- liftE $ parseJSON (head tup)++ ent <- case tup ^? ix 1 of+ Nothing -> return Nothing+ Just e -> do+ meName <- liftE $ parseJSON @(Maybe Text) e+ traverse (localE fst . getEntity) meName++ let name2rob r = do+ mrName <- liftE $ parseJSON @(Maybe Text) r+ traverse (localE snd . getRobot) mrName++ robs <- mapMaybeM name2rob (drop 2 tup)++ return $ Cell terr ent robs
+ src/Swarm/Game/Scenario/Objective.hs view
@@ -0,0 +1,195 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TemplateHaskell #-}+{-# OPTIONS_GHC -fno-warn-orphans #-}++module Swarm.Game.Scenario.Objective where++import Control.Applicative ((<|>))+import Control.Lens hiding (from, (<.>))+import Data.Aeson+import Data.Set qualified as Set+import Data.Text (Text)+import GHC.Generics (Generic)+import Swarm.Game.Scenario.Objective.Logic as L+import Swarm.Language.Pipeline (ProcessedTerm)+import Swarm.TUI.Model.Achievement.Definitions+import Swarm.Util (reflow)++------------------------------------------------------------+-- Scenario objectives+------------------------------------------------------------++data PrerequisiteConfig = PrerequisiteConfig+ { previewable :: Bool+ -- ^ Typically, only the currently "active" objectives are+ -- displayed to the user in the Goals dialog. An objective+ -- is "active" if all of its prerequisites are met.+ --+ -- However, some objectives may be "high-level", in that they may+ -- explain the broader intention behind potentially multiple+ -- prerequisites.+ --+ -- Set this to option True to display this goal in the "upcoming" section even+ -- if the objective has currently unmet prerequisites.+ , logic :: Prerequisite ObjectiveLabel+ -- ^ Boolean expression of dependencies upon other objectives. Variables in this expression+ -- are the "id"s of other objectives, and become "true" if the corresponding objective is completed.+ -- The "condition" of the objective at hand shall not be evaluated until its+ -- prerequisite expression evaluates as True.+ --+ -- Note that the achievement of these objective dependencies is+ -- persistent; once achieved, they still count even if their "condition"+ -- might not still hold. The condition is never re-evaluated once True.+ }+ deriving (Eq, Show, Generic, ToJSON)++instance FromJSON PrerequisiteConfig where+ -- Parsing JSON/YAML 'PrerequisiteConfig' has a shorthand option+ -- in which the boolean expression can be written directly,+ -- bypassing the "logic" key.+ -- Furthermore, an "Id" in a boolean expressions can be written+ -- as a bare string without needing the "id" key.+ parseJSON val = preLogic val <|> preObject val+ where+ preObject = withObject "prerequisite" $ \v ->+ PrerequisiteConfig+ <$> (v .:? "previewable" .!= False)+ <*> v .: "logic"+ preLogic = fmap (PrerequisiteConfig False) . parseJSON++-- | An objective is a condition to be achieved by a player in a+-- scenario.+data Objective = Objective+ { _objectiveGoal :: [Text]+ , _objectiveTeaser :: Maybe Text+ , _objectiveCondition :: ProcessedTerm+ , _objectiveId :: Maybe ObjectiveLabel+ , _objectiveOptional :: Bool+ , _objectivePrerequisite :: Maybe PrerequisiteConfig+ , _objectiveHidden :: Bool+ , _objectiveAchievement :: Maybe AchievementInfo+ }+ deriving (Eq, Show, Generic, ToJSON)++makeLensesWith (lensRules & generateSignatures .~ False) ''Objective++-- | An explanation of the goal of the objective, shown to the player+-- during play. It is represented as a list of paragraphs.+objectiveGoal :: Lens' Objective [Text]++-- | A very short (3-5 words) description of the goal for+-- displaying on the left side of the Objectives modal.+objectiveTeaser :: Lens' Objective (Maybe Text)++-- | A winning condition for the objective, expressed as a+-- program of type @cmd bool@. By default, this program will be+-- run to completion every tick (the usual limits on the number+-- of CESK steps per tick do not apply).+objectiveCondition :: Lens' Objective ProcessedTerm++-- | Optional name by which this objective may be referenced+-- as a prerequisite for other objectives.+objectiveId :: Lens' Objective (Maybe Text)++-- | Indicates whether the objective is not required in order+-- to "win" the scenario. Useful for (potentially hidden) achievements.+-- If the field is not supplied, it defaults to False (i.e. the+-- objective is mandatory to "win").+objectiveOptional :: Lens' Objective Bool++-- | Dependencies upon other objectives+objectivePrerequisite :: Lens' Objective (Maybe PrerequisiteConfig)++-- | Whether the goal is displayed in the UI before completion.+-- The goal will always be revealed after it is completed.+--+-- This attribute often goes along with an Achievement.+objectiveHidden :: Lens' Objective Bool++-- | An optional Achievement that is to be registered globally+-- when this objective is completed.+objectiveAchievement :: Lens' Objective (Maybe AchievementInfo)++instance FromJSON Objective where+ parseJSON = withObject "objective" $ \v ->+ Objective+ <$> (fmap . map) reflow (v .:? "goal" .!= [])+ <*> (v .:? "teaser")+ <*> (v .: "condition")+ <*> (v .:? "id")+ <*> (v .:? "optional" .!= False)+ <*> (v .:? "prerequisite")+ <*> (v .:? "hidden" .!= False)+ <*> (v .:? "achievement")++data CompletionBuckets = CompletionBuckets+ { incomplete :: [Objective]+ , completed :: [Objective]+ , unwinnable :: [Objective]+ }+ deriving (Show, Generic, FromJSON, ToJSON)++data ObjectiveCompletion = ObjectiveCompletion+ { completionBuckets :: CompletionBuckets+ -- ^ This is the authoritative "completion status"+ -- for all objectives.+ -- Note that there is a separate Set to store the+ -- completion status of prerequisite objectives, which+ -- must be carefully kept in sync with this.+ -- Those prerequisite objectives are required to have+ -- labels, but other objectives are not.+ -- Therefore only prerequisites exist in the completion+ -- map keyed by label.+ , completedIDs :: Set.Set ObjectiveLabel+ }+ deriving (Show, Generic, FromJSON, ToJSON)++-- | Concatenates all incomplete and completed objectives.+listAllObjectives :: CompletionBuckets -> [Objective]+listAllObjectives (CompletionBuckets x y z) = x <> y <> z++addCompleted :: Objective -> ObjectiveCompletion -> ObjectiveCompletion+addCompleted obj (ObjectiveCompletion buckets cmplIds) =+ ObjectiveCompletion newBuckets newCmplById+ where+ newBuckets =+ buckets+ { completed = obj : completed buckets+ }+ newCmplById = case _objectiveId obj of+ Nothing -> cmplIds+ Just lbl -> Set.insert lbl cmplIds++addUnwinnable :: Objective -> ObjectiveCompletion -> ObjectiveCompletion+addUnwinnable obj (ObjectiveCompletion buckets cmplIds) =+ ObjectiveCompletion newBuckets cmplIds+ where+ newBuckets =+ buckets+ { unwinnable = obj : unwinnable buckets+ }++setIncomplete ::+ ([Objective] -> [Objective]) ->+ ObjectiveCompletion ->+ ObjectiveCompletion+setIncomplete f (ObjectiveCompletion buckets cmplIds) =+ ObjectiveCompletion newBuckets cmplIds+ where+ newBuckets =+ buckets+ { incomplete = f $ incomplete buckets+ }++addIncomplete :: Objective -> ObjectiveCompletion -> ObjectiveCompletion+addIncomplete obj = setIncomplete (obj :)++-- | Returns the "ObjectiveCompletion" with the "incomplete" goals+-- extracted to a separate tuple member.+-- This is intended as input to a "fold".+extractIncomplete :: ObjectiveCompletion -> (ObjectiveCompletion, [Objective])+extractIncomplete oc =+ (withoutIncomplete, incompleteGoals)+ where+ incompleteGoals = incomplete $ completionBuckets oc+ withoutIncomplete = setIncomplete (const []) oc
+ src/Swarm/Game/Scenario/Objective/Graph.hs view
@@ -0,0 +1,144 @@+{-# OPTIONS_GHC -fno-warn-orphans #-}++-- | Utilities for performing graph analysis on Objective prerequisites+module Swarm.Game.Scenario.Objective.Graph where++import Control.Arrow ((&&&))+import Data.Aeson+import Data.BoolExpr (Signed (Positive))+import Data.BoolExpr qualified as BE+import Data.Graph (Graph, SCC (AcyclicSCC), graphFromEdges, stronglyConnComp)+import Data.Map (Map)+import Data.Map.Strict qualified as M+import Data.Maybe (mapMaybe)+import Data.Set qualified as Set+import Data.Text qualified as T+import Data.Tuple (swap)+import GHC.Generics (Generic)+import Swarm.Game.Scenario.Objective+import Swarm.Game.Scenario.Objective.Logic as L+import Swarm.Game.Scenario.Objective.WinCheck++-- | This is only needed for constructing a Graph,+-- which requires all nodes to have a key.+data ObjectiveId+ = Label (Signed ObjectiveLabel)+ | -- | for unlabeled objectives+ Ordinal Int+ deriving (Eq, Ord, Show, Generic, ToJSON)++data GraphInfo = GraphInfo+ { actualGraph :: Graph+ , isAcyclic :: Bool+ , sccInfo :: [SCC Objective]+ , nodeIDs :: [ObjectiveId]+ }+ deriving (Show, Generic, ToJSON)++instance ToJSON (SCC Objective) where+ toJSON = String . T.pack . show++instance ToJSON Graph where+ toJSON = String . T.pack . show++getConstFromSigned :: BE.Signed a -> a+getConstFromSigned = \case+ BE.Positive x -> x+ BE.Negative x -> x++-- | Collect all of the constants that have a negation.+-- This is necessary for enumerating all of the distinct+-- nodes when constructing a Graph, as we treat a constant+-- and its negation as distinct nodes.+getNegatedIds :: [Objective] -> Map ObjectiveLabel Objective+getNegatedIds objs =+ M.fromList $ mapMaybe f allConstants+ where+ objectivesById = getObjectivesById objs++ allPrereqExpressions = mapMaybe _objectivePrerequisite objs+ allConstants =+ mapMaybe onlyNegative+ . Set.toList+ . Set.unions+ . map (getDistinctConstants . logic)+ $ allPrereqExpressions++ f = sequenceA . \x -> (x, M.lookup x objectivesById)++ onlyNegative = \case+ BE.Negative x -> Just x+ _ -> Nothing++getObjectivesById :: [Objective] -> Map ObjectiveLabel Objective+getObjectivesById objs =+ M.fromList $+ map swap $+ mapMaybe (sequenceA . (id &&& _objectiveId)) objs++-- | Uses the textual labels for those objectives that+-- have them, and assigns arbitrary integer IDs for+-- the remaining.+--+-- Only necessary for constructing a "Graph".+assignIds :: [Objective] -> Map ObjectiveId Objective+assignIds objs =+ unlabeledObjsMap <> labeledObjsMap+ where+ objectivesById = getObjectivesById objs++ labeledObjsMap = M.mapKeys (Label . Positive) objectivesById++ unlabeledObjs = filter (null . _objectiveId) objs+ unlabeledObjsMap = M.fromList $ zipWith (\x y -> (Ordinal x, y)) [0 ..] unlabeledObjs++type Edges = [(Objective, ObjectiveId, [ObjectiveId])]++-- | NOTE: Based strictly on the goal labels, the graph could+-- potentially contain a cycle, if there exist+-- mutually-exclusive goals. That is, if goal A depends on the NOT+-- of "goal B". Goal B could then also depend on "NOT Goal A" (re-enforcing the+-- mutual-exclusivity), or it could mandate a completion order, e.g.:+-- Goal A and Goal B are simultaneously available to pursue. However, if the+-- player completes Goal B first, then it closes off the option to complete+-- Goal A. However, if Goal A is completed first, then the user is also allowed+-- to complete Goal B.+--+-- To avoid a "cycle" in this circumstance, "A" needs to exist as a distinct node+-- from "NOT A" in the graph.+makeGraph :: Edges -> Graph+makeGraph edges =+ myGraph+ where+ (myGraph, _, _) = graphFromEdges edges++makeGraphEdges :: [Objective] -> Edges+makeGraphEdges objectives =+ rootTuples <> negatedTuples+ where+ rootTuples = map f $ M.toList $ assignIds objectives+ negatedTuples = map gg $ M.toList $ getNegatedIds objectives+ gg (k, v) = (v, Label $ BE.Negative k, [])++ f (k, v) = (v, k, maybe [] (map Label . g) $ _objectivePrerequisite v)+ g = Set.toList . getDistinctConstants . logic++isAcyclicGraph :: [SCC Objective] -> Bool+isAcyclicGraph =+ all isAcyclicVertex+ where+ isAcyclicVertex = \case+ AcyclicSCC _ -> True+ _ -> False++makeGraphInfo :: ObjectiveCompletion -> GraphInfo+makeGraphInfo oc =+ GraphInfo+ (makeGraph edges)+ (isAcyclicGraph connectedComponents)+ connectedComponents+ (M.keys $ assignIds objs)+ where+ edges = makeGraphEdges objs+ connectedComponents = stronglyConnComp edges+ objs = listAllObjectives $ completionBuckets oc
+ src/Swarm/Game/Scenario/Objective/Logic.hs view
@@ -0,0 +1,46 @@+-- | A model for defining boolean expressions for Objective prerequisites.+--+-- This model is intended to be user-facing in the .yaml files, and is+-- distinct from that in 'Data.BoolExpr'.+module Swarm.Game.Scenario.Objective.Logic where++import Control.Applicative ((<|>))+import Data.Aeson+import Data.BoolExpr+import Data.Char (toLower)+import Data.List.NonEmpty (NonEmpty)+import Data.Text (Text)+import GHC.Generics (Generic)++type ObjectiveLabel = Text++-- | In contrast with the "BoolExpr" type,+-- "And" and "Or" can have /one or more/ children+-- instead of /exactly two/.+data Prerequisite a+ = And (NonEmpty (Prerequisite a))+ | Or (NonEmpty (Prerequisite a))+ | Not (Prerequisite a)+ | Id a+ deriving (Eq, Show, Generic, Functor, Foldable)++prerequisiteOptions :: Options+prerequisiteOptions =+ defaultOptions+ { sumEncoding = ObjectWithSingleField+ , constructorTagModifier = map toLower+ }++instance ToJSON (Prerequisite ObjectiveLabel) where+ toJSON = genericToJSON prerequisiteOptions++instance FromJSON (Prerequisite ObjectiveLabel) where+ parseJSON x = preString x <|> genericParseJSON prerequisiteOptions x+ where+ preString = withText "prerequisite" $ pure . Id++toBoolExpr :: Prerequisite a -> BoolExpr a+toBoolExpr (And xs) = foldr1 BAnd (fmap toBoolExpr xs)+toBoolExpr (Or xs) = foldr1 BOr (fmap toBoolExpr xs)+toBoolExpr (Not x) = BNot $ toBoolExpr x+toBoolExpr (Id x) = BConst $ pure x
+ src/Swarm/Game/Scenario/Objective/Presentation/Model.hs view
@@ -0,0 +1,117 @@+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE NoGeneralizedNewtypeDeriving #-}++-- A UI-centric model for Objective presentation.+module Swarm.Game.Scenario.Objective.Presentation.Model where++import Brick.Focus+import Brick.Widgets.List qualified as BL+import Control.Lens (makeLenses)+import Data.Aeson+import Data.List.NonEmpty (NonEmpty, nonEmpty)+import Data.List.NonEmpty qualified as NE+import Data.Map (Map)+import Data.Map qualified as M+import Data.Maybe (mapMaybe)+import GHC.Generics (Generic)+import Swarm.Game.Scenario.Objective+import Swarm.Game.Scenario.Objective.WinCheck+import Swarm.TUI.Model.Name+import Swarm.Util (listEnums)++-- | These are intended to be used as keys in a map+-- of lists of goals.+data GoalStatus+ = -- | Goals in this category have other goals as prerequisites.+ -- However, they are only displayed if the "previewable" attribute+ -- is `true`.+ Upcoming+ | -- | Goals in this category may be pursued in parallel.+ -- However, they are only displayed if the "hidden" attribute+ -- is `false`.+ Active+ | -- | A goal's programmatic condition, as well as all its prerequisites, were completed.+ -- This is a "latch" mechanism; at some point the conditions required to meet the goal may+ -- no longer hold. Nonetheless, the goal remains "completed".+ Completed+ | -- | A goal that can no longer be achieved.+ -- If this goal is not an "optional" goal, then the player+ -- also "Loses" the scenario.+ --+ -- Note that currently the only way to "Fail" a goal is by way+ -- of a negative prerequisite that was completed.+ Failed+ deriving (Show, Eq, Ord, Bounded, Enum, Generic, ToJSON, ToJSONKey)++-- | TODO: #1044 Could also add an "ObjectiveFailed" constructor...+newtype Announcement+ = ObjectiveCompleted Objective+ deriving (Show, Generic, ToJSON)++type CategorizedGoals = Map GoalStatus (NonEmpty Objective)++data GoalEntry+ = Header GoalStatus+ | Goal GoalStatus Objective++isHeader :: GoalEntry -> Bool+isHeader = \case+ Header _ -> True+ _ -> False++data GoalTracking = GoalTracking+ { announcements :: [Announcement]+ -- ^ TODO: #1044 the actual contents of these are not used yet,+ -- other than as a flag to pop up the Goal dialog.+ , goals :: CategorizedGoals+ }+ deriving (Generic, ToJSON)++data GoalDisplay = GoalDisplay+ { _goalsContent :: GoalTracking+ , _listWidget :: BL.List Name GoalEntry+ -- ^ required for maintaining the selection/navigation+ -- state among list items+ , _focus :: FocusRing Name+ }++makeLenses ''GoalDisplay++emptyGoalDisplay :: GoalDisplay+emptyGoalDisplay =+ GoalDisplay+ (GoalTracking mempty mempty)+ (BL.list (GoalWidgets ObjectivesList) mempty 1)+ (focusRing $ map GoalWidgets listEnums)++hasAnythingToShow :: GoalTracking -> Bool+hasAnythingToShow (GoalTracking ann g) = not (null ann && null g)++hasMultipleGoals :: GoalTracking -> Bool+hasMultipleGoals gt =+ goalCount > 1+ where+ goalCount = sum . M.elems . M.map NE.length . goals $ gt++constructGoalMap :: Bool -> ObjectiveCompletion -> CategorizedGoals+constructGoalMap isCheating objectiveCompletion@(ObjectiveCompletion buckets _) =+ M.fromList $+ mapMaybe (traverse nonEmpty) categoryList+ where+ categoryList =+ [ (Upcoming, displayableInactives)+ , (Active, suppressHidden activeGoals)+ , (Completed, completed buckets)+ , (Failed, unwinnable buckets)+ ]++ displayableInactives =+ suppressHidden $+ filter (maybe False previewable . _objectivePrerequisite) inactiveGoals++ suppressHidden =+ if isCheating+ then id+ else filter $ not . _objectiveHidden++ (activeGoals, inactiveGoals) = partitionActiveObjectives objectiveCompletion
+ src/Swarm/Game/Scenario/Objective/Presentation/Render.hs view
@@ -0,0 +1,91 @@+{-# LANGUAGE OverloadedStrings #-}++-- Display logic for Objectives.+module Swarm.Game.Scenario.Objective.Presentation.Render where++import Brick hiding (Direction, Location)+import Brick.Focus+import Brick.Widgets.Center+import Brick.Widgets.List qualified as BL+import Control.Applicative ((<|>))+import Control.Lens hiding (Const, from)+import Data.List.NonEmpty qualified as NE+import Data.Map.Strict qualified as M+import Data.Maybe (listToMaybe)+import Data.Vector qualified as V+import Swarm.Game.Scenario.Objective+import Swarm.Game.Scenario.Objective.Presentation.Model+import Swarm.TUI.Attr+import Swarm.TUI.Model.Name+import Swarm.TUI.View.Util++makeListWidget :: GoalTracking -> BL.List Name GoalEntry+makeListWidget (GoalTracking _announcements categorizedObjs) =+ BL.listMoveTo 1 $ BL.list (GoalWidgets ObjectivesList) (V.fromList objList) 1+ where+ objList = concatMap f $ M.toList categorizedObjs+ f (h, xs) = Header h : map (Goal h) (NE.toList xs)++renderGoalsDisplay :: GoalDisplay -> Widget Name+renderGoalsDisplay gd =+ if hasMultiple+ then+ hBox+ [ leftSide+ , hLimitPercent 70 $ padLeft (Pad 2) goalElaboration+ ]+ else goalElaboration+ where+ hasMultiple = hasMultipleGoals $ gd ^. goalsContent+ lw = _listWidget gd+ fr = _focus gd+ leftSide =+ hLimitPercent 30 $+ padAll 1 $+ vBox+ [ hCenter $ str "Goals"+ , padAll 1 $+ vLimit 10 $+ withFocusRing fr (BL.renderList drawGoalListItem) lw+ ]++ -- Adds very subtle coloring to indicate focus switch+ highlightIfFocused = case (hasMultiple, focusGetCurrent fr) of+ (True, Just (GoalWidgets GoalSummary)) -> withAttr lightCyanAttr+ _ -> id++ -- Note: An extra "padRight" is inserted to account for the vertical scrollbar,+ -- whether or not it appears.+ goalElaboration =+ clickable (GoalWidgets GoalSummary) $+ maybeScroll ModalViewport $+ maybe emptyWidget (padAll 1 . padRight (Pad 1) . highlightIfFocused . singleGoalDetails . snd) $+ BL.listSelectedElement lw++getCompletionIcon :: Objective -> GoalStatus -> Widget Name+getCompletionIcon obj = \case+ Upcoming -> withAttr yellowAttr $ txt " ○ "+ Active -> withAttr cyanAttr $ txt " ○ "+ Failed -> withAttr redAttr $ txt " ● "+ Completed -> withAttr colorAttr $ txt " ● "+ where+ colorAttr =+ if obj ^. objectiveHidden+ then magentaAttr+ else greenAttr++drawGoalListItem ::+ Bool ->+ GoalEntry ->+ Widget Name+drawGoalListItem _isSelected e = case e of+ Header gs -> withAttr boldAttr $ str $ show gs+ Goal gs obj -> getCompletionIcon obj gs <+> titleWidget+ where+ textSource = obj ^. objectiveTeaser <|> obj ^. objectiveId <|> listToMaybe (obj ^. objectiveGoal)+ titleWidget = maybe (txt "?") withEllipsis textSource++singleGoalDetails :: GoalEntry -> Widget Name+singleGoalDetails = \case+ Header _gs -> displayParagraphs [" "]+ Goal _gs obj -> displayParagraphs $ obj ^. objectiveGoal
+ src/Swarm/Game/Scenario/Objective/Validation.hs view
@@ -0,0 +1,50 @@+{-# LANGUAGE OverloadedStrings #-}++-- | Validity checking for Objective prerequisites+module Swarm.Game.Scenario.Objective.Validation where++import Control.Monad (unless)+import Data.Foldable (for_, toList)+import Data.Graph (stronglyConnComp)+import Data.Maybe (mapMaybe)+import Data.Set qualified as Set+import Data.Text qualified as T+import Swarm.Game.Scenario.Objective+import Swarm.Game.Scenario.Objective.Graph+import Swarm.Util (quote)+import Witch (into)++-- | Performs monadic validation before returning+-- the "pure" construction of a wrapper record.+-- This validation entails:+-- 1) Ensuring that all goal references utilized in prerequisites+-- actually exist+-- 2) Ensuring that the graph of dependencies is acyclic.+validateObjectives ::+ MonadFail m =>+ [Objective] ->+ m [Objective]+validateObjectives objectives = do+ for_ objectives $ \x -> case _objectivePrerequisite x of+ Just p ->+ unless (null remaining) $+ fail . into @String $+ T.unwords+ [ "Reference to undefined objective(s)"+ , T.intercalate ", " (map quote $ Set.toList remaining) <> "."+ , "Defined are:"+ , T.intercalate ", " (map quote $ Set.toList allIds)+ ]+ where+ refs = Set.fromList $ toList $ logic p+ remaining = Set.difference refs allIds+ Nothing -> return ()++ unless (isAcyclicGraph connectedComponents) $+ fail . into @String $+ T.unwords ["There are dependency cycles in the prerequisites."]++ return objectives+ where+ connectedComponents = stronglyConnComp $ makeGraphEdges objectives+ allIds = Set.fromList $ mapMaybe _objectiveId objectives
+ src/Swarm/Game/Scenario/Objective/WinCheck.hs view
@@ -0,0 +1,83 @@+-- | Utilities to check whether conditions are met for a game win/loss.+module Swarm.Game.Scenario.Objective.WinCheck where++import Data.Aeson+import Data.BoolExpr qualified as BE+import Data.BoolExpr.Simplify qualified as Simplify+import Data.List (partition)+import Data.Map qualified as M+import Data.Set (Set)+import Data.Set qualified as Set+import GHC.Generics (Generic)+import Swarm.Game.Scenario.Objective+import Swarm.Game.Scenario.Objective.Logic as L++-- | We have "won" if all of the "unwinnable" or remaining "incomplete" objectives are "optional".+didWin :: ObjectiveCompletion -> Bool+didWin oc = all _objectiveOptional $ incomplete buckets <> unwinnable buckets+ where+ buckets = completionBuckets oc++-- | We have "lost" if any of the "unwinnable" objectives not "optional".+didLose :: ObjectiveCompletion -> Bool+didLose oc = not $ all _objectiveOptional $ unwinnable buckets+ where+ buckets = completionBuckets oc++isPrereqsSatisfied :: ObjectiveCompletion -> Objective -> Bool+isPrereqsSatisfied completions =+ maybe True f . _objectivePrerequisite+ where+ f = BE.evalBoolExpr getTruth . L.toBoolExpr . logic++ getTruth :: ObjectiveLabel -> Bool+ getTruth label = Set.member label $ completedIDs completions++isUnwinnablePrereq :: Set ObjectiveLabel -> Prerequisite ObjectiveLabel -> Bool+isUnwinnablePrereq completedObjectives =+ Simplify.cannotBeTrue . Simplify.replace boolMap . L.toBoolExpr+ where+ boolMap =+ M.fromList $+ map (,True) $+ Set.toList completedObjectives++isUnwinnable :: ObjectiveCompletion -> Objective -> Bool+isUnwinnable completions obj =+ maybe False (isUnwinnablePrereq (completedIDs completions) . logic) $ _objectivePrerequisite obj++-- | The first element of the returned tuple consists of "active" objectives,+-- the second element "inactive".+partitionActiveObjectives :: ObjectiveCompletion -> ([Objective], [Objective])+partitionActiveObjectives oc =+ partition (isPrereqsSatisfied oc) $+ incomplete $+ completionBuckets oc++getActiveObjectives :: ObjectiveCompletion -> [Objective]+getActiveObjectives =+ fst . partitionActiveObjectives++-- | For debugging only (via Web API)+data PrereqSatisfaction = PrereqSatisfaction+ { objective :: Objective+ , deps :: Set (BE.Signed ObjectiveLabel)+ , prereqsSatisfied :: Bool+ }+ deriving (Generic, ToJSON)++-- | Used only by the web interface for debugging+getSatisfaction :: ObjectiveCompletion -> [PrereqSatisfaction]+getSatisfaction oc =+ map f $+ listAllObjectives $+ completionBuckets oc+ where+ f y =+ PrereqSatisfaction+ y+ (maybe mempty (getDistinctConstants . logic) $ _objectivePrerequisite y)+ (isPrereqsSatisfied oc y)++getDistinctConstants :: (Ord a) => Prerequisite a -> Set (BE.Signed a)+getDistinctConstants = Set.fromList . BE.constants . toBoolExpr
+ src/Swarm/Game/Scenario/RobotLookup.hs view
@@ -0,0 +1,51 @@+{-# LANGUAGE DerivingVia #-}+{-# LANGUAGE OverloadedStrings #-}++module Swarm.Game.Scenario.RobotLookup where++import Control.Lens hiding (from, (<.>))+import Data.Map (Map)+import Data.Map qualified as M+import Data.Text (Text)+import Swarm.Game.Entity+import Swarm.Game.Robot (TRobot, trobotName)+import Swarm.Util.Yaml++------------------------------------------------------------+-- Robot map+------------------------------------------------------------++-- | A robot template paired with its definition's index within+-- the Scenario file+type IndexedTRobot = (Int, TRobot)++-- | A map from names to robots, used to look up robots in scenario+-- descriptions.+type RobotMap = Map Text IndexedTRobot++-- | Create a 'RobotMap' from a list of robot templates.+buildRobotMap :: [TRobot] -> RobotMap+buildRobotMap rs = M.fromList $ zipWith (\x y -> (view trobotName y, (x, y))) [0 ..] rs++------------------------------------------------------------+-- Lookup utilities+------------------------------------------------------------++-- | Look up a thing by name, throwing a parse error if it is not+-- found.+getThing :: String -> (Text -> m -> Maybe a) -> Text -> ParserE m a+getThing thing lkup name = do+ m <- getE+ case lkup name m of+ Nothing -> fail $ "Unknown " <> thing <> " name: " ++ show name+ Just a -> return a++-- | Look up an entity by name in an 'EntityMap', throwing a parse+-- error if it is not found.+getEntity :: Text -> ParserE EntityMap Entity+getEntity = getThing "entity" lookupEntityName++-- | Look up a robot by name in a 'RobotMap', throwing a parse error+-- if it is not found.+getRobot :: Text -> ParserE RobotMap IndexedTRobot+getRobot = getThing "robot" M.lookup
+ src/Swarm/Game/Scenario/Style.hs view
@@ -0,0 +1,37 @@+module Swarm.Game.Scenario.Style where++import Data.Aeson+import Data.Set (Set)+import Data.Text (Text)+import GHC.Generics (Generic)++data StyleFlag+ = Standout+ | Italic+ | Strikethrough+ | Underline+ | ReverseVideo+ | Blink+ | Dim+ | Bold+ deriving (Eq, Ord, Show, Generic)++styleFlagJsonOptions :: Options+styleFlagJsonOptions =+ defaultOptions+ { sumEncoding = UntaggedValue+ }++instance FromJSON StyleFlag where+ parseJSON = genericParseJSON styleFlagJsonOptions++newtype HexColor = HexColor Text+ deriving (Eq, Show, Generic, FromJSON)++data CustomAttr = CustomAttr+ { name :: String+ , fg :: Maybe HexColor+ , bg :: Maybe HexColor+ , style :: Maybe (Set StyleFlag)+ }+ deriving (Eq, Show, Generic, FromJSON)
+ src/Swarm/Game/Scenario/WorldDescription.hs view
@@ -0,0 +1,64 @@+{-# LANGUAGE DerivingVia #-}+{-# LANGUAGE OverloadedStrings #-}++module Swarm.Game.Scenario.WorldDescription where++import Data.Aeson.Key qualified as Key+import Data.Aeson.KeyMap (KeyMap)+import Data.Aeson.KeyMap qualified as KeyMap+import Data.Text (Text)+import Data.Text qualified as T+import Data.Yaml as Y+import Swarm.Game.Entity+import Swarm.Game.Scenario.Cell+import Swarm.Game.Scenario.RobotLookup+import Swarm.Util.Location+import Swarm.Util.Yaml+import Witch (into)++------------------------------------------------------------+-- World description+------------------------------------------------------------++-- | A world palette maps characters to 'Cell' values.+newtype WorldPalette e = WorldPalette+ {unPalette :: KeyMap (PCell e)}+ deriving (Eq, Show)++instance FromJSONE (EntityMap, RobotMap) (WorldPalette Entity) where+ parseJSONE = withObjectE "palette" $ fmap WorldPalette . mapM parseJSONE++-- | A description of a world parsed from a YAML file.+-- This type is parameterized to accommodate Cells that+-- utilize a less stateful Entity type.+data PWorldDescription e = WorldDescription+ { defaultTerrain :: Maybe (PCell e)+ , offsetOrigin :: Bool+ , palette :: WorldPalette e+ , ul :: Location+ , area :: [[PCell e]]+ }+ deriving (Eq, Show)++type WorldDescription = PWorldDescription Entity++instance FromJSONE (EntityMap, RobotMap) WorldDescription where+ parseJSONE = withObjectE "world description" $ \v -> do+ pal <- v ..:? "palette" ..!= WorldPalette mempty+ WorldDescription+ <$> v ..:? "default"+ <*> liftE (v .:? "offset" .!= False)+ <*> pure pal+ <*> liftE (v .:? "upperleft" .!= origin)+ <*> liftE ((v .:? "map" .!= "") >>= paintMap pal)++-- | "Paint" a world map using a 'WorldPalette', turning it from a raw+-- string into a nested list of 'Cell' values by looking up each+-- character in the palette, failing if any character in the raw map+-- is not contained in the palette.+paintMap :: MonadFail m => WorldPalette e -> Text -> m [[PCell e]]+paintMap pal = traverse (traverse toCell . into @String) . T.lines+ where+ toCell c = case KeyMap.lookup (Key.fromString [c]) (unPalette pal) of+ Nothing -> fail $ "Char not in world palette: " ++ show c+ Just cell -> return cell
src/Swarm/Game/ScenarioInfo.hs view
@@ -49,7 +49,7 @@ import Control.Carrier.Lift (Lift, sendIO) import Control.Carrier.Throw.Either (Throw, runThrow, throwError) import Control.Lens hiding (from, (<.>))-import Control.Monad (unless, when)+import Control.Monad (filterM, unless, when) import Data.Aeson ( Options (..), defaultOptions,@@ -59,7 +59,7 @@ ) import Data.Char (isSpace, toLower) import Data.Function (on)-import Data.List (intercalate, stripPrefix, (\\))+import Data.List (intercalate, isPrefixOf, stripPrefix, (\\)) import Data.Map (Map) import Data.Map qualified as M import Data.Maybe (isJust)@@ -90,20 +90,20 @@ data ScenarioStatus = NotStarted | InProgress- { -- | Time when the scenario was started including time zone.- _scenarioStarted :: ZonedTime- , -- | Time elapsed until quitting the scenario.- _scenarioElapsed :: NominalDiffTime- , -- | Ticks elapsed until quitting the scenario.- _scenarioElapsedTicks :: Integer+ { _scenarioStarted :: ZonedTime+ -- ^ Time when the scenario was started including time zone.+ , _scenarioElapsed :: NominalDiffTime+ -- ^ Time elapsed until quitting the scenario.+ , _scenarioElapsedTicks :: Integer+ -- ^ Ticks elapsed until quitting the scenario. } | Complete- { -- | Time when the scenario was started including time zone.- _scenarioStarted :: ZonedTime- , -- | Time elapsed until quitting the scenario.- _scenarioElapsed :: NominalDiffTime- , -- | Ticks elapsed until quitting the scenario.- _scenarioElapsedTicks :: Integer+ { _scenarioStarted :: ZonedTime+ -- ^ Time when the scenario was started including time zone.+ , _scenarioElapsed :: NominalDiffTime+ -- ^ Time elapsed until quitting the scenario.+ , _scenarioElapsedTicks :: Integer+ -- ^ Ticks elapsed until quitting the scenario. } deriving (Eq, Ord, Show, Read, Generic) @@ -256,11 +256,14 @@ False -> do when (dirName /= "Testing") $ sendIO . putStrLn $- "Warning: no " <> orderFileName <> " file found in " <> dirName+ "Warning: no "+ <> orderFileName+ <> " file found in "+ <> dirName <> ", using alphabetical order" return Nothing True -> Just . filter (not . null) . lines <$> sendIO (readFile orderFile)- fs <- sendIO $ keepYamlOrDirectory <$> listDirectory dir+ fs <- sendIO $ keepYamlOrPublicDirectory dir =<< listDirectory dir case morder of Just order -> do@@ -269,25 +272,38 @@ unless (null missing) $ sendIO . putStr . unlines $- ( "Warning: while processing " <> (dirName </> orderFileName) <> ": files not listed in "+ ( "Warning: while processing "+ <> (dirName </> orderFileName)+ <> ": files not listed in " <> orderFileName <> " will be ignored"- ) :- map (" - " <>) missing+ )+ : map (" - " <>) missing unless (null dangling) $ sendIO . putStr . unlines $- ( "Warning: while processing " <> (dirName </> orderFileName)+ ( "Warning: while processing "+ <> (dirName </> orderFileName) <> ": nonexistent files will be ignored"- ) :- map (" - " <>) dangling+ )+ : map (" - " <>) dangling Nothing -> pure () -- Only keep the files from 00-ORDER.txt that actually exist. let morder' = filter (`elem` fs) <$> morder SC morder' . M.fromList <$> mapM (\item -> (item,) <$> loadScenarioItem em (dir </> item)) fs where- keepYamlOrDirectory = filter (\f -> takeExtensions f `elem` ["", ".yaml"])+ -- Keep only files which are .yaml files or directories that start+ -- with something other than an underscore.+ keepYamlOrPublicDirectory = filterM . isCatalogEntry++ -- Whether the directory or file should be included in the scenario catalog.+ isCatalogEntry d f = do+ isDir <- doesDirectoryExist $ d </> f+ return $+ if isDir+ then not $ "_" `isPrefixOf` f+ else takeExtensions f == ".yaml" -- | How to transform scenario path to save path. scenarioPathToSavePath :: FilePath -> FilePath -> FilePath
src/Swarm/Game/State.hs view
@@ -1,4 +1,3 @@-{-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE RecordWildCards #-}@@ -20,10 +19,12 @@ -- * Game state record and related types ViewCenterRule (..), REPLStatus (..),+ WinStatus (..), WinCondition (..),+ ObjectiveCompletion (..), _NoWinCondition, _WinConditions,- _Won,+ Announcement (..), RunStatus (..), Seed, GameState,@@ -32,6 +33,8 @@ creativeMode, winCondition, winSolution,+ gameAchievements,+ announcementQueue, runStatus, paused, robotMap,@@ -114,18 +117,17 @@ import Data.Array (Array, listArray) import Data.Bifunctor (first) import Data.Foldable (toList)-import Data.Int (Int64)+import Data.Int (Int32) import Data.IntMap (IntMap) import Data.IntMap qualified as IM import Data.IntSet (IntSet) import Data.IntSet qualified as IS import Data.IntSet.Lens (setOf) import Data.List (partition, sortOn)-import Data.List.NonEmpty (NonEmpty) import Data.List.NonEmpty qualified as NE import Data.Map (Map) import Data.Map qualified as M-import Data.Maybe (fromMaybe, isJust, mapMaybe)+import Data.Maybe (fromMaybe, isJust, isNothing, mapMaybe) import Data.Sequence (Seq ((:<|))) import Data.Sequence qualified as Seq import Data.Set qualified as S@@ -134,8 +136,7 @@ import Data.Text.IO qualified as T (readFile) import Data.Time (getZonedTime) import GHC.Generics (Generic)-import Linear-import Swarm.Game.CESK (emptyStore, initMachine)+import Swarm.Game.CESK (emptyStore, finalValue, initMachine) import Swarm.Game.Entity import Swarm.Game.Recipe ( Recipe,@@ -145,9 +146,10 @@ reqRecipeMap, ) import Swarm.Game.Robot+import Swarm.Game.Scenario.Objective+import Swarm.Game.Scenario.Objective.Presentation.Model import Swarm.Game.ScenarioInfo import Swarm.Game.Terrain (TerrainType (..))-import Swarm.Game.Value (Value) import Swarm.Game.World (Coords (..), WorldFun (..), locToCoords, worldFunFromArray) import Swarm.Game.World qualified as W import Swarm.Game.WorldGen (Seed, findGoodOrigin, testWorld2FromArray)@@ -155,10 +157,14 @@ import Swarm.Language.Context qualified as Ctx import Swarm.Language.Pipeline (ProcessedTerm) import Swarm.Language.Pipeline.QQ (tmQ)-import Swarm.Language.Syntax (Const, Term (TText), allConst)+import Swarm.Language.Syntax (Const, Term' (TText), allConst) import Swarm.Language.Typed (Typed (Typed)) import Swarm.Language.Types+import Swarm.Language.Value (Value)+import Swarm.TUI.Model.Achievement.Attainment+import Swarm.TUI.Model.Achievement.Definitions import Swarm.Util (getDataFileNameSafe, getElemsInArea, isRightOr, manhattan, uniq, (<+=), (<<.=), (?))+import Swarm.Util.Location import System.Clock qualified as Clock import System.Random (StdGen, mkStdGen, randomRIO) import Witch (into)@@ -175,7 +181,7 @@ -- world viewport. data ViewCenterRule = -- | The view should be centered on an absolute position.- VCLocation (V2 Int64)+ VCLocation Location | -- | The view should be centered on a certain robot. VCRobot RID deriving (Eq, Ord, Show, Generic, FromJSON, ToJSON)@@ -186,6 +192,7 @@ data REPLStatus = -- | The REPL is not doing anything actively at the moment. -- We persist the last value and its type though.+ -- INVARIANT: the Value stored here is not a VResult. REPLDone (Maybe (Typed Value)) | -- | A command entered at the REPL is currently being run. The -- 'Polytype' represents the type of the expression that was@@ -194,15 +201,28 @@ REPLWorking (Typed (Maybe Value)) deriving (Eq, Show, Generic, FromJSON, ToJSON) +data WinStatus+ = -- | There are one or more objectives remaining that the player+ -- has not yet accomplished.+ Ongoing+ | -- | The player has won.+ -- The boolean indicates whether they have+ -- already been congratulated.+ Won Bool+ | -- | The player has completed certain "goals" that preclude+ -- (via negative prerequisites) the completion of all of the+ -- required goals.+ -- The boolean indicates whether they have+ -- already been informed.+ Unwinnable Bool+ deriving (Show, Generic, FromJSON, ToJSON)+ data WinCondition = -- | There is no winning condition. NoWinCondition- | -- | There are one or more objectives remaining that the player- -- has not yet accomplished.- WinConditions (NonEmpty Objective)- | -- | The player has won. The boolean indicates whether they have- -- already been congratulated.- Won Bool+ | -- | NOTE: It is possible to continue to achieve "optional" objectives+ -- even after the game has been won (or deemed unwinnable).+ WinConditions WinStatus ObjectiveCompletion deriving (Show, Generic, FromJSON, ToJSON) makePrisms ''WinCondition@@ -256,6 +276,8 @@ { _creativeMode :: Bool , _winCondition :: WinCondition , _winSolution :: Maybe ProcessedTerm+ , _gameAchievements :: Map GameplayAchievement Attainment+ , _announcementQueue :: Seq Announcement , _runStatus :: RunStatus , _robotMap :: IntMap Robot , -- A set of robots to consider for the next game tick. It is guaranteed to@@ -272,7 +294,7 @@ -- Waiting robots for a given time are a list because it is cheaper to -- append to a list than to a Set. _waitingRobots :: Map Integer [RID]- , _robotsByLocation :: Map (V2 Int64) IntSet+ , _robotsByLocation :: Map Location IntSet , _allDiscoveredEntities :: Inventory , _availableRecipes :: Notifications (Recipe Entity) , _availableCommands :: Notifications Const@@ -290,7 +312,7 @@ , _knownEntities :: [Text] , _world :: W.World Int Entity , _viewCenterRule :: ViewCenterRule- , _viewCenter :: V2 Int64+ , _viewCenter :: Location , _needsRedraw :: Bool , _replStatus :: REPLStatus , _replNextValueIndex :: Integer@@ -332,6 +354,16 @@ -- and to show help to cheaters (or testers). winSolution :: Lens' GameState (Maybe ProcessedTerm) +-- | Map of in-game achievements that were attained+gameAchievements :: Lens' GameState (Map GameplayAchievement Attainment)++-- | A queue of global announcments.+-- Note that this is distinct from the "messageQueue",+-- which is for messages emitted by robots.+--+-- Note that we put the newest entry to the right.+announcementQueue :: Lens' GameState (Seq Announcement)+ -- | The current 'RunStatus'. runStatus :: Lens' GameState RunStatus @@ -339,7 +371,7 @@ paused :: Getter GameState Bool paused = to (\s -> s ^. runStatus /= Running) --- | All the robots that currently exist in the game, indexed by name.+-- | All the robots that currently exist in the game, indexed by ID. robotMap :: Lens' GameState (IntMap Robot) -- | The names of all robots that currently exist in the game, indexed by@@ -351,10 +383,10 @@ -- location of a robot changes, or a robot is created or destroyed. -- Fortunately, there are relatively few ways for these things to -- happen.-robotsByLocation :: Lens' GameState (Map (V2 Int64) IntSet)+robotsByLocation :: Lens' GameState (Map Location IntSet) -- | Get a list of all the robots at a particular location.-robotsAtLocation :: V2 Int64 -> GameState -> [Robot]+robotsAtLocation :: Location -> GameState -> [Robot] robotsAtLocation loc gs = mapMaybe (`IM.lookup` (gs ^. robotMap)) . maybe [] IS.toList@@ -362,8 +394,9 @@ . view robotsByLocation $ gs --- | Get robots in manhattan distastance from location.-robotsInArea :: V2 Int64 -> Int64 -> GameState -> [Robot]+-- | Get all the robots within a given Manhattan distance from a+-- location.+robotsInArea :: Location -> Int32 -> GameState -> [Robot] robotsInArea o d gs = map (rm IM.!) rids where rm = gs ^. robotMap@@ -446,7 +479,7 @@ -- modified directly, since it is calculated automatically from the -- 'viewCenterRule'. To modify the view center, either set the -- 'viewCenterRule', or use 'modifyViewCenter'.-viewCenter :: Getter GameState (V2 Int64)+viewCenter :: Getter GameState Location viewCenter = to _viewCenter -- | Whether the world view needs to be redrawn.@@ -549,14 +582,14 @@ messageIsRecent :: GameState -> LogEntry -> Bool messageIsRecent gs e = e ^. leTime >= gs ^. ticks - 1 -messageIsFromNearby :: V2 Int64 -> LogEntry -> Bool+messageIsFromNearby :: Location -> LogEntry -> Bool messageIsFromNearby l e = manhattan l (e ^. leLocation) <= hearingDistance -- | Given a current mapping from robot names to robots, apply a -- 'ViewCenterRule' to derive the location it refers to. The result -- is @Maybe@ because the rule may refer to a robot which does not -- exist.-applyViewCenterRule :: ViewCenterRule -> IntMap Robot -> Maybe (V2 Int64)+applyViewCenterRule :: ViewCenterRule -> IntMap Robot -> Maybe Location applyViewCenterRule (VCLocation l) _ = Just l applyViewCenterRule (VCRobot name) m = m ^? at name . _Just . robotLocation @@ -579,7 +612,7 @@ -- current value. Note that this also modifies the 'viewCenterRule' -- to match. After calling this function the 'viewCenterRule' will -- specify a particular location, not a robot.-modifyViewCenter :: (V2 Int64 -> V2 Int64) -> GameState -> GameState+modifyViewCenter :: (Location -> Location) -> GameState -> GameState modifyViewCenter update g = g & case g ^. viewCenterRule of@@ -588,10 +621,10 @@ -- | Given a width and height, compute the region, centered on the -- 'viewCenter', that should currently be in view.-viewingRegion :: GameState -> (Int64, Int64) -> (W.Coords, W.Coords)+viewingRegion :: GameState -> (Int32, Int32) -> (W.Coords, W.Coords) viewingRegion g (w, h) = (W.Coords (rmin, cmin), W.Coords (rmax, cmax)) where- V2 cx cy = g ^. viewCenter+ Location cx cy = g ^. viewCenter (rmin, rmax) = over both (+ (-cy - h `div` 2)) (0, h - 1) (cmin, cmax) = over both (+ (cx - w `div` 2)) (0, w - 1) @@ -682,7 +715,7 @@ ------------------------------------------------------------ -- | Create an initial game state record, first loading entities and--- recipies from disk.+-- recipes from disk. initGameState :: ExceptT Text IO GameState initGameState = do let guardRight what i = i `isRightOr` (\e -> "Failed to " <> what <> ": " <> e)@@ -705,6 +738,10 @@ { _creativeMode = False , _winCondition = NoWinCondition , _winSolution = Nothing+ , -- This does not need to be initialized with anything,+ -- since the master list of achievements is stored in UIState+ _gameAchievements = mempty+ , _announcementQueue = mempty , _runStatus = Running , _robotMap = IM.empty , _robotsByLocation = M.empty@@ -727,7 +764,7 @@ , _knownEntities = [] , _world = W.emptyWorld (fromEnum StoneT) , _viewCenterRule = VCRobot 0- , _viewCenter = V2 0 0+ , _viewCenter = origin , _needsRedraw = False , _replStatus = REPLDone Nothing , _replNextValueIndex = 0@@ -775,13 +812,13 @@ , _knownEntities = scenario ^. scenarioKnown , _world = theWorld theSeed , _viewCenterRule = VCRobot baseID- , _viewCenter = V2 0 0+ , _viewCenter = origin , _needsRedraw = False- , -- When starting base with the run flag, REPL status must be set to working,- -- otherwise the store of definition cells is not saved (see #333)- _replStatus = case toRun of- Nothing -> REPLDone Nothing- Just _ -> REPLWorking (Typed Nothing PolyUnit mempty)+ , -- When the base starts out running a program, the REPL status must be set to working,+ -- otherwise the store of definition cells is not saved (see #333, #838)+ _replStatus = case running of+ False -> REPLDone Nothing+ True -> REPLWorking (Typed Nothing PolyUnit mempty) , _replNextValueIndex = 0 , _messageQueue = Empty , _focusedRobotID = baseID@@ -829,6 +866,9 @@ zipWith instantiateRobot [baseID ..] robotsByBasePrecedence -- If the --run flag was used, use it to replace the CESK machine of the -- robot whose id is 0, i.e. the first robot listed in the scenario.+ -- Note that this *replaces* any program the base robot otherwise+ -- would have run (i.e. any program specified in the program: field+ -- of the scenario description). & ix baseID . machine %~ case getCodeToRun <$> toRun of Nothing -> id@@ -838,16 +878,20 @@ %~ case scenario ^. scenarioCreative of False -> id True -> union (fromElems (map (0,) things))- & ix baseID . installedDevices+ & ix baseID . equippedDevices %~ case scenario ^. scenarioCreative of False -> id True -> const (fromList devices) + running = case robotList of+ [] -> False+ (base : _) -> isNothing (finalValue (base ^. machine))+ -- Initial list of available commands = all commands enabled by- -- devices in inventory or installed; and commands that require no+ -- devices in inventory or equipped; and commands that require no -- capability. allCapabilities r =- inventoryCapabilities (r ^. installedDevices)+ inventoryCapabilities (r ^. equippedDevices) <> inventoryCapabilities (r ^. robotInventory) initialCaps = mconcat $ map allCapabilities robotList initialCommands =@@ -857,7 +901,12 @@ (genRobots, wf) = buildWorld em (scenario ^. scenarioWorld) theWorld = W.newWorld . wf- theWinCondition = maybe NoWinCondition WinConditions (NE.nonEmpty (scenario ^. scenarioObjectives))+ theWinCondition =+ maybe+ NoWinCondition+ (\x -> WinConditions Ongoing (ObjectiveCompletion (CompletionBuckets (NE.toList x) mempty mempty) mempty))+ (NE.nonEmpty (scenario ^. scenarioObjectives))+ initGensym = length robotList - 1 addRecipesWith f gRs = IM.unionWith (<>) (f $ scenario ^. scenarioRecipes) (g ^. gRs) @@ -873,7 +922,7 @@ worldGrid :: [[(TerrainType, Maybe Entity)]] worldGrid = (map . map) (cellTerrain &&& cellEntity) area - worldArray :: Array (Int64, Int64) (TerrainType, Maybe Entity)+ worldArray :: Array (Int32, Int32) (TerrainType, Maybe Entity) worldArray = listArray ((ulr, ulc), (ulr + rs - 1, ulc + cs - 1)) (concat worldGrid) wf = case defaultTerrain of
src/Swarm/Game/Step.hs view
@@ -18,37 +18,38 @@ module Swarm.Game.Step where import Control.Applicative (liftA2)+import Control.Arrow ((&&&)) import Control.Carrier.Error.Either (runError) import Control.Carrier.State.Lazy import Control.Carrier.Throw.Either (ThrowC, runThrow) import Control.Effect.Error import Control.Effect.Lens import Control.Effect.Lift-import Control.Lens as Lens hiding (Const, from, parts, use, uses, view, (%=), (+=), (.=), (<+=), (<>=))-import Control.Monad (forM, forM_, guard, msum, unless, when)+import Control.Lens as Lens hiding (Const, distrib, from, parts, use, uses, view, (%=), (+=), (.=), (<+=), (<>=))+import Control.Monad (foldM, forM, forM_, guard, msum, unless, when) import Data.Array (bounds, (!)) import Data.Bifunctor (second) import Data.Bool (bool)-import Data.Containers.ListUtils (nubOrd)+import Data.Char (chr, ord) import Data.Either (partitionEithers, rights) import Data.Foldable (asum, traverse_) import Data.Functor (void)-import Data.Int (Int64) import Data.IntMap qualified as IM import Data.IntSet qualified as IS-import Data.List (find)+import Data.List (find, sortOn) import Data.List qualified as L-import Data.List.NonEmpty (NonEmpty ((:|)))-import Data.List.NonEmpty qualified as NE import Data.Map qualified as M-import Data.Maybe (catMaybes, fromMaybe, isNothing, listToMaybe)+import Data.Maybe (catMaybes, fromMaybe, isJust, isNothing, listToMaybe, mapMaybe)+import Data.Ord (Down (Down))+import Data.Sequence ((><)) import Data.Sequence qualified as Seq import Data.Set (Set) import Data.Set qualified as S import Data.Text (Text) import Data.Text qualified as T+import Data.Time (getZonedTime) import Data.Tuple (swap)-import Linear (V2 (..), zero, (^+^))+import Linear (zero) import Swarm.Game.CESK import Swarm.Game.Display import Swarm.Game.Entity hiding (empty, lookup, singleton, union)@@ -56,18 +57,23 @@ import Swarm.Game.Exception import Swarm.Game.Recipe import Swarm.Game.Robot-import Swarm.Game.Scenario (objectiveCondition)+import Swarm.Game.Scenario.Objective qualified as OB+import Swarm.Game.Scenario.Objective.WinCheck qualified as WC import Swarm.Game.State-import Swarm.Game.Value import Swarm.Game.World qualified as W import Swarm.Language.Capability import Swarm.Language.Context hiding (delete) import Swarm.Language.Pipeline import Swarm.Language.Pipeline.QQ (tmQ)+import Swarm.Language.Pretty (prettyText) import Swarm.Language.Requirement qualified as R import Swarm.Language.Syntax import Swarm.Language.Typed (Typed (..))+import Swarm.Language.Value+import Swarm.TUI.Model.Achievement.Attainment+import Swarm.TUI.Model.Achievement.Definitions import Swarm.Util+import Swarm.Util.Location import System.Clock (TimeSpec) import System.Clock qualified import System.Random (UniformRange, uniformR)@@ -126,30 +132,134 @@ -- Possibly see if the winning condition for the current objective is met. wc <- use winCondition case wc of- WinConditions (obj :| objs) -> do+ WinConditions winState oc -> do g <- get @GameState-- -- Execute the win condition check *hypothetically*: i.e. in a- -- fresh CESK machine, using a copy of the current game state.- v <- runThrow @Exn . evalState @GameState g $ evalPT (obj ^. objectiveCondition)- case v of- -- Log exceptions in the message queue so we can check for them in tests- Left exn -> do- em <- use entityMap- time <- use ticks- let h = hypotheticalRobot (Out VUnit emptyStore []) 0- hid = view robotID h- hn = view robotName h- farAway = V2 maxBound maxBound- let m = LogEntry time ErrorTrace hn hid farAway $ formatExn em exn- emitMessage m- Right (VBool True) -> winCondition .= maybe (Won False) WinConditions (NE.nonEmpty objs)- _ -> return ()+ em <- use entityMap+ hypotheticalWinCheck em g winState oc _ -> return () -- Advance the game time by one. ticks += 1 +-- | An accumulator for folding over the incomplete+-- objectives to evaluate for their completion+data CompletionsWithExceptions = CompletionsWithExceptions+ { exceptions :: [Text]+ , completions :: ObjectiveCompletion+ , completionAnnouncementQueue :: [OB.Objective]+ -- ^ Upon completion, an objective is enqueued.+ -- It is dequeued when displayed on the UI.+ }++-- | Execute the win condition check *hypothetically*: i.e. in a+-- fresh CESK machine, using a copy of the current game state.+--+-- The win check is performed only on "active" goals; that is,+-- the goals that are currently unmet and have had all of their+-- prerequisites satisfied.+-- Note that it may be possible, while traversing through the+-- goal list, for one goal to be met earlier in the list that+-- happens to be a prerequisite later in the traversal. This+-- is why:+-- 1) We must not pre-filter the goals to be traversed based+-- on satisfied prerequisites (i.e. we cannot use the+-- "getActiveObjectives" function).+-- 2) The traversal order must be "reverse topological" order, so+-- that prerequisites are evaluated before dependent goals.+-- 3) The iteration needs to be a "fold", so that state is updated+-- after each element.+hypotheticalWinCheck ::+ (Has (State GameState) sig m, Has (Lift IO) sig m) =>+ EntityMap ->+ GameState ->+ WinStatus ->+ ObjectiveCompletion ->+ m ()+hypotheticalWinCheck em g ws oc = do+ -- We can fully and accurately evaluate the new state of the objectives DAG+ -- in a single pass, so long as we visit it in reverse topological order.+ --+ -- N.B. The "reverse" is essential due to the re-population of the+ -- "incomplete" goal list by cons-ing.+ finalAccumulator <-+ foldM foldFunc initialAccumulator $+ reverse incompleteGoals++ let newWinState = case ws of+ Ongoing -> getNextWinState $ completions finalAccumulator+ _ -> ws++ winCondition .= WinConditions newWinState (completions finalAccumulator)++ case newWinState of+ Unwinnable _ -> do+ currentTime <- sendIO getZonedTime+ gameAchievements+ %= M.insertWith+ (<>)+ LoseScenario+ (Attainment (GameplayAchievement LoseScenario) Nothing currentTime)+ _ -> return ()++ announcementQueue %= (>< Seq.fromList (map ObjectiveCompleted $ completionAnnouncementQueue finalAccumulator))++ mapM_ handleException $ exceptions finalAccumulator+ where+ getNextWinState completedObjs+ | WC.didWin completedObjs = Won False+ | WC.didLose completedObjs = Unwinnable False+ | otherwise = Ongoing++ (withoutIncomplete, incompleteGoals) = OB.extractIncomplete oc+ initialAccumulator = CompletionsWithExceptions [] withoutIncomplete []++ -- All of the "incomplete" goals have been emptied from the initial accumulator, and+ -- these are what we iterate over with the fold.+ -- Each iteration, we either place the goal back into the "incomplete" bucket, or+ -- we determine that it has been met or impossible and place it into the "completed"+ -- or "unwinnable" bucket, respectively.+ foldFunc (CompletionsWithExceptions exnTexts currentCompletions announcements) obj = do+ v <-+ if WC.isPrereqsSatisfied currentCompletions obj+ then runThrow @Exn . evalState @GameState g $ evalPT $ obj ^. OB.objectiveCondition+ else return $ Right $ VBool False+ let simplified = simplifyResult $ stripVResult <$> v+ return $ case simplified of+ Left exnText ->+ CompletionsWithExceptions+ (exnText : exnTexts)+ currentCompletions+ announcements+ Right boolResult ->+ CompletionsWithExceptions+ exnTexts+ (modifyCompletions obj currentCompletions)+ (modifyAnnouncements announcements)+ where+ (modifyCompletions, modifyAnnouncements)+ | boolResult = (OB.addCompleted, (obj :))+ | WC.isUnwinnable currentCompletions obj = (OB.addUnwinnable, id)+ | otherwise = (OB.addIncomplete, id)++ simplifyResult = \case+ Left exn -> Left $ formatExn em exn+ Right (VBool x) -> Right x+ Right val ->+ Left $+ T.unwords+ [ "Non boolean value:"+ , prettyValue val+ , "real:"+ , T.pack (show val)+ ]++ -- Log exceptions in the message queue so we can check for them in tests+ handleException exnText = do+ m <- evalState @Robot h $ createLogEntry (ErrorTrace Critical) exnText+ emitMessage m+ where+ h = hypotheticalRobot (Out VUnit emptyStore []) 0+ evalPT :: (Has (Lift IO) sig m, Has (Throw Exn) sig m, Has (State GameState) sig m) => ProcessedTerm ->@@ -206,12 +316,12 @@ return a -- | Get the entity (if any) at a given location.-entityAt :: (Has (State GameState) sig m) => V2 Int64 -> m (Maybe Entity)+entityAt :: (Has (State GameState) sig m) => Location -> m (Maybe Entity) entityAt loc = zoomWorld (W.lookupEntityM @Int (W.locToCoords loc)) -- | Modify the entity (if any) at a given location. updateEntityAt ::- (Has (State GameState) sig m) => V2 Int64 -> (Maybe Entity -> Maybe Entity) -> m ()+ (Has (State GameState) sig m) => Location -> (Maybe Entity -> Maybe Entity) -> m () updateEntityAt loc upd = zoomWorld (W.updateM @Int (W.locToCoords loc) upd) -- | Get the robot with a given ID.@@ -312,7 +422,7 @@ robotCaps <- use robotCapabilities let hasCaps = cap `S.member` robotCaps (sys || creative || hasCaps)- `holdsOr` Incapable FixByInstall (R.singletonCap cap) (TConst c)+ `holdsOr` Incapable FixByEquip (R.singletonCap cap) (TConst c) -- | Test whether the current robot has a given capability (either -- because it has a device which gives it that capability, or it is a@@ -330,12 +440,16 @@ (Has (State Robot) sig m, Has (State GameState) sig m, Has (Throw Exn) sig m) => Capability -> Term -> m () hasCapabilityFor cap term = do h <- hasCapability cap- h `holdsOr` Incapable FixByInstall (R.singletonCap cap) term+ h `holdsOr` Incapable FixByEquip (R.singletonCap cap) term -- | Create an exception about a command failing. cmdExn :: Const -> [Text] -> Exn-cmdExn c parts = CmdFailed c (T.unwords parts)+cmdExn c parts = CmdFailed c (T.unwords parts) Nothing +-- | Create an exception about a command failing, with an achievement+cmdExnWithAchievement :: Const -> [Text] -> GameplayAchievement -> Exn+cmdExnWithAchievement c parts a = CmdFailed c (T.unwords parts) $ Just a+ -- | Raise an exception about a command failing with a formatted error message. raise :: (Has (Throw Exn) sig m) => Const -> [Text] -> m a raise c parts = throwError (cmdExn c parts)@@ -369,7 +483,7 @@ tickRobotRec :: (Has (State GameState) sig m, Has (Lift IO) sig m) => Robot -> m Robot tickRobotRec r | isActive r && (r ^. runningAtomic || r ^. tickSteps > 0) =- stepRobot r >>= tickRobotRec+ stepRobot r >>= tickRobotRec | otherwise = return r -- | Single-step a robot by decrementing its 'tickSteps' counter and@@ -377,14 +491,28 @@ stepRobot :: (Has (State GameState) sig m, Has (Lift IO) sig m) => Robot -> m Robot stepRobot r = do (r', cesk') <- runState (r & tickSteps -~ 1) (stepCESK (r ^. machine))+ -- sendIO $ appendFile "out.txt" (prettyString cesk' ++ "\n") return $ r' & machine .~ cesk' +-- replace some entity in the world with another entity+updateWorld ::+ (Has (State GameState) sig m, Has (Throw Exn) sig m) =>+ Const ->+ WorldUpdate Entity ->+ m ()+updateWorld c (ReplaceEntity loc eThen down) = do+ w <- use world+ let eNow = W.lookupEntity (W.locToCoords loc) w+ if Just eThen /= eNow+ then throwError $ cmdExn c ["The", eThen ^. entityName, "is not there."]+ else do+ world %= W.update (W.locToCoords loc) (const down)+ pure ()+ -- | The main CESK machine workhorse. Given a robot, look at its CESK -- 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 cesk = case cesk of- -- (sendIO $ appendFile "out.txt" (prettyCESK cesk)) >>- ------------------------------------------------------------ -- Evaluation @@ -395,13 +523,14 @@ if wakeupTime <= time then stepCESK cesk' else return cesk- Out v s (FImmediate wf rf : k) -> do- wc <- worldUpdate wf <$> use world+ Out v s (FImmediate cmd wf rf : k) -> do+ wc <- runError $ mapM_ (updateWorld cmd) wf case wc of Left exn -> return $ Up exn s k- Right wo -> do- robotInventory %= robotUpdateInventory rf- world .= wo+ Right () -> do+ forM_ rf $ \case+ AddEntity c e -> robotInventory %= E.insertCount c e+ LearnEntity e -> robotInventory %= E.insertCount 0 e needsRedraw .= True stepCESK (Out v s k) @@ -458,7 +587,7 @@ Out v2 s (FApp (VCApp c args) : k) | not (isCmd c) && arity c == length args + 1 ->- evalConst c (reverse (v2 : args)) s k+ evalConst c (reverse (v2 : args)) s k | otherwise -> return $ Out (VCApp c (v2 : args)) s k Out _ s (FApp _ : _) -> badMachineState s "FApp of non-function" -- To evaluate non-recursive let expressions, we start by focusing on the@@ -527,6 +656,15 @@ runningAtomic .= False return $ Out v s k + -- Machinery for implementing the 'meetAll' command.+ -- First case: done meeting everyone.+ Out b s (FMeetAll _ [] : k) -> return $ Out b s k+ -- More still to meet: apply the function to the current value b and+ -- then the next robot id. This will result in a command which we+ -- execute, discard any generated environment, and then pass the+ -- result to continue meeting the rest of the robots.+ Out b s (FMeetAll f (rid : rids) : k) ->+ return $ Out b s (FApp f : FArg (TRobot rid) empty : FExec : FDiscardEnv : FMeetAll f rids : k) -- To execute a bind expression, evaluate and execute the first -- command, and remember the second for execution later. Out (VBind mx c1 c2 e) s (FExec : k) -> return $ In c1 e s (FExec : FBind mx c2 e : k)@@ -558,6 +696,9 @@ -- Or, if a command completes with no environment, but there is a -- previous environment to union with, just use that environment. Out v s (FUnionEnv e : k) -> return $ Out (VResult v e) s k+ -- If there's an explicit DiscardEnv frame, throw away any returned environment.+ Out (VResult v _) s (FDiscardEnv : k) -> return $ Out v s k+ Out v s (FDiscardEnv : k) -> return $ Out v s k -- If the top of the continuation stack contains a 'FLoadEnv' frame, -- it means we are supposed to load up the resulting definition -- environment, store, and type and capability contexts into the robot's@@ -594,21 +735,34 @@ -- First, if we were running a try block but evaluation completed normally, -- just ignore the try block and continue. Out v s (FTry {} : k) -> return $ Out v s k- -- If an exception rises all the way to the top level without being- -- handled, turn it into an error message.-- -- HOWEVER, we have to make sure to check that the robot has the- -- 'log' capability which is required to collect and view logs.- --- -- Notice how we call resetBlackholes on the store, so that any- -- cells which were in the middle of being evaluated will be reset. Up exn s [] -> do+ -- Here, an exception has risen all the way to the top level without being+ -- handled.+ case exn of+ CmdFailed _ _ (Just a) -> do+ currentTime <- sendIO getZonedTime+ scenarioPath <- use currentScenarioPath+ gameAchievements+ %= M.insertWith+ (<>)+ a+ (Attainment (GameplayAchievement a) scenarioPath currentTime)+ _ -> return ()++ -- If an exception rises all the way to the top level without being+ -- handled, turn it into an error message.++ -- HOWEVER, we have to make sure to check that the robot has the+ -- 'log' capability which is required to collect and view logs.+ --+ -- Notice how we call resetBlackholes on the store, so that any+ -- cells which were in the middle of being evaluated will be reset. let s' = resetBlackholes s h <- hasCapability CLog em <- use entityMap if h then do- void $ traceLog ErrorTrace (formatExn em exn)+ void $ traceLog (ErrorTrace Error) (formatExn em exn) return $ Out VUnit s [] else return $ Out VUnit s' [] -- Fatal errors, capability errors, and infinite loop errors can't@@ -630,7 +784,7 @@ let msg' = T.unlines [ T.append "Bad machine state in stepRobot: " msg- , from (prettyCESK cesk)+ , prettyText cesk ] in return $ Up (Fatal msg') s [] @@ -681,7 +835,7 @@ -- | Construct a "seed robot" from entity, time range and position, -- and add it to the world. It has low priority and will be covered -- by placed entities.-addSeedBot :: Has (State GameState) sig m => Entity -> (Integer, Integer) -> V2 Int64 -> TimeSpec -> m ()+addSeedBot :: Has (State GameState) sig m => Entity -> (Integer, Integer) -> Location -> TimeSpec -> m () addSeedBot e (minT, maxT) loc ts = void $ addTRobot $@@ -691,7 +845,7 @@ "seed" ["A growing seed."] (Just loc)- (V2 0 0)+ zero ( defaultEntityDisplay '.' & displayAttr .~ (e ^. entityDisplay . displayAttr) & displayPriority .~ 0@@ -734,14 +888,14 @@ return $ Waiting (time + d) (Out VUnit s k) _ -> badConst Selfdestruct -> do- destroyIfNotBase+ destroyIfNotBase $ Just AttemptSelfDestructBase flagRedraw return $ Out VUnit s k Move -> do -- Figure out where we're going loc <- use robotLocation orient <- use robotOrientation- let nextLoc = loc ^+^ (orient ? zero)+ let nextLoc = loc .+^ (orient ? zero) checkMoveAhead nextLoc $ MoveFailure { failIfBlocked = ThrowExn@@ -755,7 +909,7 @@ target <- getRobotWithinTouch rid -- either change current robot or one in robot map let oldLoc = target ^. robotLocation- nextLoc = V2 (fromIntegral x) (fromIntegral y)+ nextLoc = Location (fromIntegral x) (fromIntegral y) onTarget rid $ do checkMoveAhead nextLoc $@@ -834,44 +988,37 @@ return $ Out VUnit s k _ -> badConst- Install -> case vs of- [VRobot otherID, VText itemName] -> do- -- Make sure the other robot exists and is close- _other <- getRobotWithinTouch otherID-- item <- ensureItem itemName "install"-+ Equip -> case vs of+ [VText itemName] -> do+ item <- ensureItem itemName "equip" myID <- use robotID focusedID <- use focusedRobotID- case otherID == myID of- -- We have to special case installing something on ourselves- -- for the same reason as Give.- True -> do- -- Don't do anything if the robot already has the device.- already <- use (installedDevices . to (`E.contains` item))- unless already $ do- installedDevices %= insert item- robotInventory %= delete item-- -- Flag the UI for a redraw if we are currently showing our inventory- when (focusedID == myID) flagRedraw- False -> do- let otherDevices = robotMap . at otherID . _Just . installedDevices- already <- use $ pre (otherDevices . to (`E.contains` item))- unless (already == Just True) $ do- robotMap . at otherID . _Just . installedDevices %= insert item- robotInventory %= delete item+ -- Don't do anything if the robot already has the device.+ already <- use (equippedDevices . to (`E.contains` item))+ unless already $ do+ equippedDevices %= insert item+ robotInventory %= delete item - -- Flag the UI for a redraw if we are currently showing- -- either robot's inventory- when (focusedID == myID || focusedID == otherID) flagRedraw+ -- Flag the UI for a redraw if we are currently showing our inventory+ when (focusedID == myID) flagRedraw return $ Out VUnit s k _ -> badConst+ Unequip -> case vs of+ [VText itemName] -> do+ item <- ensureEquipped itemName+ myID <- use robotID+ focusedID <- use focusedRobotID+ equippedDevices %= delete item+ robotInventory %= insert item+ -- Flag the UI for a redraw if we are currently showing our inventory+ when (focusedID == myID) flagRedraw+ return $ Out VUnit s k+ _ -> badConst Make -> case vs of [VText name] -> do inv <- use robotInventory- ins <- use installedDevices+ ins <- use equippedDevices em <- use entityMap e <- lookupEntityName name em@@ -893,7 +1040,7 @@ let displayMissingCount mc = \case MissingInput -> from (show mc)- MissingCatalyst -> "not installed"+ MissingCatalyst -> "not equipped" displayMissingIngredient (MissingIngredient mk mc me) = " - " <> me ^. entityName <> " (" <> displayMissingCount mc mk <> ")" displayMissingIngredients xs = L.intercalate ["OR"] (map displayMissingIngredient <$> xs)@@ -913,16 +1060,16 @@ -- take recipe inputs from inventory and add outputs after recipeTime robotInventory .= invTaken traverse_ (updateDiscoveredEntities . snd) (recipe ^. recipeOutputs)- finishCookingRecipe recipe (WorldUpdate Right) (RobotUpdate changeInv)+ finishCookingRecipe recipe [] (map (uncurry AddEntity) changeInv) _ -> badConst Has -> case vs of [VText name] -> do inv <- use robotInventory return $ Out (VBool ((> 0) $ countByName name inv)) s k _ -> badConst- Installed -> case vs of+ Equipped -> case vs of [VText name] -> do- inv <- use installedDevices+ inv <- use equippedDevices return $ Out (VBool ((> 0) $ countByName name inv)) s k _ -> badConst Count -> case vs of@@ -931,8 +1078,20 @@ return $ Out (VInt (fromIntegral $ countByName name inv)) s k _ -> badConst Whereami -> do- V2 x y <- use robotLocation+ loc <- use robotLocation+ let Location x y = loc return $ Out (VPair (VInt (fromIntegral x)) (VInt (fromIntegral y))) s k+ Heading -> do+ mh <- use robotOrientation+ -- In general, (1) entities might not have an orientation, and+ -- (2) even if they do, orientation is a general vector, which+ -- might not correspond to a cardinal direction. We could make+ -- 'heading' return a 'maybe dir' i.e. 'unit + dir', or return a+ -- vector of type 'int * int', but those would both be annoying+ -- for players in the vast majority of cases. We rather choose+ -- to just return the direction 'down' in any case where we don't+ -- otherwise have anything reasonable to return.+ return $ Out (VDir (fromMaybe (DRelative DDown) $ mh >>= toDirection)) s k Time -> do t <- use ticks return $ Out (VInt t) s k@@ -940,18 +1099,19 @@ [VDir d] -> do rname <- use robotName inv <- use robotInventory- ins <- use installedDevices+ ins <- use equippedDevices - let toyDrill = lookupByName "drill" ins- metalDrill = lookupByName "metal drill" ins- insDrill = listToMaybe $ metalDrill <> toyDrill+ let equippedDrills = extantElemsWithCapability CDrill ins+ -- Heuristic: choose the drill with the more elaborate name.+ -- E.g. "metal drill" vs. "drill"+ preferredDrill = listToMaybe $ sortOn (Down . T.length . (^. entityName)) equippedDrills - drill <- insDrill `isJustOr` Fatal "Drill is required but not installed?!"+ drill <- preferredDrill `isJustOr` Fatal "Drill is required but not equipped?!" let directionText = case d of- DDown -> "under"- DForward -> "ahead of"- DBack -> "behind"+ DRelative DDown -> "under"+ DRelative DForward -> "ahead of"+ DRelative DBack -> "behind" _ -> dirSyntax (dirInfo d) <> " of" (nextLoc, nextME) <- lookInDirection d@@ -974,19 +1134,28 @@ `isJustOrFail` ["You don't have the ingredients to drill", indefinite (nextE ^. entityName) <> "."] let (out, down) = L.partition ((`hasProperty` Portable) . snd) outs- changeInv =- flip (L.foldl' (flip $ uncurry insertCount)) out- . flip (L.foldl' (flip $ insertCount 0)) (map snd down)- changeWorld = changeWorld' nextE nextLoc down+ let learn = map (LearnEntity . snd) down+ let gain = map (uncurry AddEntity) out + newEntity <- case down of+ [] -> pure Nothing+ [(1, de)] -> pure $ Just de+ _ -> throwError $ Fatal "Bad recipe:\n more than one unmovable entity produced."+ let changeWorld =+ ReplaceEntity+ { updatedLoc = nextLoc+ , originalEntity = nextE+ , newEntity = newEntity+ }+ -- take recipe inputs from inventory and add outputs after recipeTime robotInventory .= invTaken- finishCookingRecipe recipe (WorldUpdate changeWorld) (RobotUpdate changeInv)+ finishCookingRecipe recipe [changeWorld] (learn <> gain) _ -> badConst Blocked -> do loc <- use robotLocation orient <- use robotOrientation- let nextLoc = loc ^+^ (orient ? zero)+ let nextLoc = loc .+^ (orient ? zero) me <- entityAt nextLoc return $ Out (VBool (maybe False (`hasProperty` Unwalkable) me)) s k Scan -> case vs of@@ -1004,7 +1173,7 @@ Knows -> case vs of [VText name] -> do inv <- use robotInventory- ins <- use installedDevices+ ins <- use equippedDevices let allKnown = inv `E.union` ins let knows = case E.lookupByName name allKnown of [] -> False@@ -1043,7 +1212,7 @@ As -> case vs of [VRobot rid, prog] -> do -- Get the named robot and current game state- r <- robotWithID rid >>= (`isJustOrFail` ["There is no robot with ID", from (show rid)])+ r <- robotWithID rid >>= (`isJustOrFail` ["There is no actor with ID", from (show rid)]) g <- get @GameState -- Execute the given program *hypothetically*: i.e. in a fresh@@ -1082,7 +1251,7 @@ m <- traceLog Said msg -- current robot will inserted to robot set, so it needs the log emitMessage m let addLatestClosest rl = \case- Seq.Empty -> Seq.Empty+ Seq.Empty -> Seq.singleton m es Seq.:|> e | e ^. leTime < m ^. leTime -> es |> e |> m | manhattan rl (e ^. leLocation) > manhattan rl (m ^. leLocation) -> es |> m@@ -1129,7 +1298,7 @@ [VRobot rid] -> do _ <- robotWithID rid- >>= (`isJustOrFail` ["There is no robot with ID", from (show rid), "to view."])+ >>= (`isJustOrFail` ["There is no actor with ID", from (show rid), "to view."]) -- Only the base can actually change the view in the UI. Other robots can -- execute this command but it does nothing (at least for now).@@ -1176,6 +1345,10 @@ Nothing -> return $ Out (VBool False) s k Just e -> return $ Out (VBool (T.toLower (e ^. entityName) == T.toLower name)) s k _ -> badConst+ Isempty -> do+ loc <- use robotLocation+ me <- entityAt loc+ return $ Out (VBool (isNothing me)) s k Self -> do rid <- use robotID return $ Out (VRobot rid) s k@@ -1184,6 +1357,23 @@ rid <- use robotID return $ Out (VRobot (fromMaybe rid mp)) s k Base -> return $ Out (VRobot 0) s k+ Meet -> do+ loc <- use robotLocation+ rid <- use robotID+ g <- get @GameState+ let neighbor =+ find ((/= rid) . (^. robotID)) -- pick one other than ourself+ . sortOn (manhattan loc . (^. robotLocation)) -- prefer closer+ $ robotsInArea loc 1 g -- all robots within Manhattan distance 1+ return $ Out (VInj (isJust neighbor) (maybe VUnit (VRobot . (^. robotID)) neighbor)) s k+ MeetAll -> case vs of+ [f, b] -> do+ loc <- use robotLocation+ rid <- use robotID+ g <- get @GameState+ let neighborIDs = filter (/= rid) . map (^. robotID) $ robotsInArea loc 1 g+ return $ Out b s (FMeetAll f neighborIDs : k)+ _ -> badConst Whoami -> case vs of [] -> do name <- use robotName@@ -1258,7 +1448,7 @@ -- check if robot exists childRobot <- robotWithID childRobotID- >>= (`isJustOrFail` ["There is no robot with ID", from (show childRobotID) <> "."])+ >>= (`isJustOrFail` ["There is no actor with ID", from (show childRobotID) <> "."]) -- check that current robot is not trying to reprogram self myID <- use robotID@@ -1279,11 +1469,11 @@ -- Figure out if we can supply what the target robot requires, -- and if so, what is needed.- (toInstall, toGive) <-+ (toEquip, toGive) <- checkRequirements (r ^. robotInventory) (childRobot ^. robotInventory)- (childRobot ^. installedDevices)+ (childRobot ^. equippedDevices) cmd "The target robot" FixByObtain@@ -1297,7 +1487,7 @@ -- Provision the target robot with any required devices and -- inventory that are lacking.- provisionChild childRobotID (fromList . S.toList $ toInstall) toGive+ provisionChild childRobotID (fromList . S.toList $ toEquip) toGive -- Finally, re-activate the reprogrammed target robot. activateRobot childRobotID@@ -1331,7 +1521,7 @@ r <- get @Robot pid <- use robotID - (toInstall, toGive) <-+ (toEquip, toGive) <- checkRequirements (r ^. robotInventory) E.empty E.empty cmd "You" FixByObtain -- Pick a random display name.@@ -1339,8 +1529,9 @@ createdAt <- getNow -- Construct the new robot and add it to the world.+ parentCtx <- use robotContext newRobot <-- addTRobot $+ addTRobot . (trobotContext .~ parentCtx) $ mkRobot () (Just pid)@@ -1359,7 +1550,7 @@ createdAt -- Provision the new robot with the necessary devices and inventory.- provisionChild (newRobot ^. robotID) (fromList . S.toList $ toInstall) toGive+ provisionChild (newRobot ^. robotID) (fromList . S.toList $ toEquip) toGive -- Flag the world for a redraw and return the name of the newly constructed robot. flagRedraw@@ -1373,16 +1564,16 @@ case mtarget of Nothing -> return $ Out VUnit s k -- Nothing to salvage Just target -> do- -- Copy the salvaged robot's installed devices into its inventory, in preparation+ -- Copy the salvaged robot's equipped devices into its inventory, in preparation -- for transferring it.- let salvageInventory = E.union (target ^. robotInventory) (target ^. installedDevices)+ let salvageInventory = E.union (target ^. robotInventory) (target ^. equippedDevices) robotMap . at (target ^. robotID) . traverse . robotInventory .= salvageInventory let salvageItems = concatMap (\(n, e) -> replicate n (e ^. entityName)) (E.elems salvageInventory) numItems = length salvageItems -- Copy over the salvaged robot's log, if we have one- inst <- use installedDevices+ inst <- use equippedDevices em <- use entityMap creative <- use creativeMode logger <-@@ -1409,7 +1600,10 @@ giveItem item = TApp (TApp (TConst Give) (TRobot ourID)) (TText item) -- Reprogram and activate the salvaged robot- robotMap . at (target ^. robotID) . traverse . machine+ robotMap+ . at (target ^. robotID)+ . traverse+ . machine .= In giveInventory empty emptyStore [FExec] activateRobot (target ^. robotID) @@ -1433,9 +1627,15 @@ processTerm (into @Text f) `isRightOr` \err -> cmdExn Run ["Error in", fileName, "\n", err] - return $ case mt of- Nothing -> Out VUnit s k- Just t -> initMachine' t empty s k+ case mt of+ Nothing -> return $ Out VUnit s k+ Just t@(ProcessedTerm _ _ reqCtx) -> do+ -- Add the reqCtx from the ProcessedTerm to the current robot's defReqs.+ -- See #827 for an explanation of (1) why this is needed, (2) why+ -- it's slightly technically incorrect, and (3) why it is still way+ -- better than what we had before.+ robotContext . defReqs <>= reqCtx+ return $ initMachine' t empty s k _ -> badConst Not -> case vs of [VBool b] -> return $ Out (VBool (not b)) s k@@ -1475,6 +1675,19 @@ Concat -> case vs of [VText v1, VText v2] -> return $ Out (VText (v1 <> v2)) s k _ -> badConst+ CharAt -> case vs of+ [VInt i, VText t]+ | i < 0 || i >= fromIntegral (T.length t) ->+ raise CharAt ["Index", prettyValue (VInt i), "out of bounds for length", from @String $ show (T.length t)]+ | otherwise -> return $ Out (VInt . fromIntegral . ord . T.index t . fromIntegral $ i) s k+ _ -> badConst+ ToChar -> case vs of+ [VInt i]+ | i < 0 || i > fromIntegral (ord (maxBound :: Char)) ->+ raise ToChar ["Value", prettyValue (VInt i), "is an invalid character code"]+ | otherwise ->+ return $ Out (VText . T.singleton . chr . fromIntegral $ i) s k+ _ -> badConst AppF -> let msg = "The operator '$' should only be a syntactic sugar and removed in elaboration:\n" in throwError . Fatal $ msg <> badConstMsg@@ -1486,28 +1699,37 @@ badConstMsg = T.unlines [ "Bad application of execConst:"- , from (prettyCESK (Out (VCApp c (reverse vs)) s k))+ , T.pack (show c)+ , T.pack (show (reverse vs))+ , prettyText (Out (VCApp c (reverse vs)) s k) ] - finishCookingRecipe :: HasRobotStepState sig m => Recipe e -> WorldUpdate -> RobotUpdate -> m CESK+ finishCookingRecipe :: HasRobotStepState sig m => Recipe e -> [WorldUpdate Entity] -> [RobotUpdate] -> m CESK finishCookingRecipe r wf rf = do time <- use ticks let remTime = r ^. recipeTime return . (if remTime <= 1 then id else Waiting (remTime + time)) $- Out VUnit s (FImmediate wf rf : k)+ Out VUnit s (FImmediate c wf rf : k) - lookInDirection :: HasRobotStepState sig m => Direction -> m (V2 Int64, Maybe Entity)+ lookInDirection :: HasRobotStepState sig m => Direction -> m (Location, Maybe Entity) lookInDirection d = do loc <- use robotLocation orient <- use robotOrientation when (isCardinal d) $ hasCapabilityFor COrient (TDir d)- let nextLoc = loc ^+^ applyTurn d (orient ? zero)+ let nextLoc = loc .+^ applyTurn d (orient ? zero) (nextLoc,) <$> entityAt nextLoc++ ensureEquipped :: HasRobotStepState sig m => Text -> m Entity+ ensureEquipped itemName = do+ inst <- use equippedDevices+ listToMaybe (lookupByName itemName inst)+ `isJustOrFail` ["You don't have a", indefinite itemName, "equipped."]+ ensureItem :: HasRobotStepState sig m => Text -> Text -> m Entity ensureItem itemName action = do -- First, make sure we know about the entity. inv <- use robotInventory- inst <- use installedDevices+ inst <- use equippedDevices item <- asum (map (listToMaybe . lookupByName itemName) [inv, inst]) `isJustOrFail` ["What is", indefinite itemName <> "?"]@@ -1527,14 +1749,14 @@ -- both 'Build' and 'Reprogram'. -- -- It is given as inputs the parent robot inventory, the inventory- -- and installed devices of the child (these will be empty in the+ -- and equipped devices of the child (these will be empty in the -- case of 'Build'), and the command to be run (along with a few -- inputs to configure any error messages to be generated). -- -- Throws an exception if it's not possible to set up the child -- robot with the things it needs to execute the given program. -- Otherwise, returns a pair consisting of the set of devices to be- -- installed, and the inventory that should be transferred from+ -- equipped, and the inventory that should be transferred from -- parent to child. checkRequirements :: HasRobotStepState sig m =>@@ -1557,74 +1779,94 @@ -- See #349 (R.Requirements (S.toList -> caps) (S.toList -> devNames) reqInvNames, _capCtx) = R.requirements currentContext cmd - -- Check that all required device names exist, and fail with- -- an exception if not- devs <- forM devNames $ \devName ->+ -- Check that all required device names exist (fail with+ -- an exception if not) and convert them to 'Entity' values.+ (devs :: [Entity]) <- forM devNames $ \devName -> E.lookupEntityName devName em `isJustOrFail` ["Unknown device required: " <> devName] - -- Check that all required inventory entity names exist, and fail- -- with an exception if not- reqElems <- forM (M.assocs reqInvNames) $ \(eName, n) ->+ -- Check that all required inventory entity names exist (fail with+ -- an exception if not) and convert them to 'Entity' values, with+ -- an associated count for each.+ (reqInv :: Inventory) <- fmap E.fromElems . forM (M.assocs reqInvNames) $ \(eName, n) -> (n,) <$> ( E.lookupEntityName eName em `isJustOrFail` ["Unknown entity required: " <> eName] )- let reqInv = E.fromElems reqElems - let -- List of possible devices per requirement. Devices for- -- required capabilities come first, then singleton devices- -- that are required directly. This order is important since- -- later we zip required capabilities with this list to figure- -- out which capabilities are missing.- capDevices = map (`deviceForCap` em) caps ++ map (: []) devs+ let -- List of possible devices per requirement. For the+ -- requirements that stem from a required capability, we+ -- remember the capability alongside the possible devices, to+ -- help with later error message generation.+ possibleDevices :: [(Maybe Capability, [Entity])]+ possibleDevices =+ map (Just &&& (`deviceForCap` em)) caps -- Possible devices for capabilities+ ++ map ((Nothing,) . (: [])) devs -- Outright required devices -- A device is OK if it is available in the inventory of the- -- parent robot, or already installed in the child robot.+ -- parent robot, or already equipped in the child robot.+ deviceOK :: Entity -> Bool deviceOK d = parentInventory `E.contains` d || childDevices `E.contains` d - -- take a pair of device sets providing capabilities that is- -- split into (AVAIL,MISSING) and if there are some available- -- ignore missing because we only need them for error message- ignoreOK ([], miss) = ([], miss)- ignoreOK (ds, _miss) = (ds, [])-- (deviceSets, missingDeviceSets) =- Lens.over both (nubOrd . map S.fromList) . unzip $- map (ignoreOK . L.partition deviceOK) capDevices-- formatDevices = T.intercalate " or " . map (^. entityName) . S.toList- -- capabilities not provided by any device in inventory- missingCaps = S.fromList . map fst . filter (null . snd) $ zip caps deviceSets+ -- Partition each list of possible devices into a set of+ -- available devices and a set of unavailable devices.+ -- There's a problem if some capability is required but no+ -- devices that provide it are available. In that case we can+ -- print an error message, using the second set as a list of+ -- suggestions.+ partitionedDevices :: [(Set Entity, Set Entity)]+ partitionedDevices =+ map (Lens.over both S.fromList . L.partition deviceOK . snd) possibleDevices - alreadyInstalled = S.fromList . map snd . E.elems $ childDevices+ -- Devices equipped on the child, as a Set instead of an+ -- Inventory for convenience.+ alreadyEquipped :: Set Entity+ alreadyEquipped = S.fromList . map snd . E.elems $ childDevices - -- Figure out what is missing from the required inventory+ -- Figure out what is still missing of the required inventory:+ -- the required inventory, less any inventory the child robot+ -- already has. missingChildInv = reqInv `E.difference` childInventory if creative- then -- In creative mode, just return ALL the devices- return (S.unions (map S.fromList capDevices) `S.difference` alreadyInstalled, missingChildInv)+ then+ return+ ( -- In creative mode, just equip ALL the devices+ -- providing each required capability (because, why+ -- not?). But don't re-equip any that are already+ -- equipped.+ S.unions (map (S.fromList . snd) possibleDevices) `S.difference` alreadyEquipped+ , -- Conjure the necessary missing inventory out of thin+ -- air.+ missingChildInv+ ) else do- -- check if robot has all devices to execute new command- all null missingDeviceSets- `holdsOrFail` ( singularSubjectVerb subject "do" :- "not have required devices, please" :- formatIncapableFix fixI <> ":" :- (("\n - " <>) . formatDevices <$> filter (not . null) missingDeviceSets)+ -- First, check that devices actually exist AT ALL to provide every+ -- required capability. If not, we will generate an error message saying+ -- something like "missing capability X but no device yet provides it".+ let capsWithNoDevice = mapMaybe fst . filter (null . snd) $ possibleDevices+ null capsWithNoDevice+ `holdsOr` Incapable fixI (R.Requirements (S.fromList capsWithNoDevice) S.empty M.empty) cmd++ -- Now, ensure there is at least one device available to be+ -- equipped for each requirement.+ let missingDevices = map snd . filter (null . fst) $ partitionedDevices+ null missingDevices+ `holdsOrFail` ( singularSubjectVerb subject "do"+ : "not have required devices, please"+ : formatIncapableFix fixI <> ":"+ : (("\n - " <>) . formatDevices <$> missingDevices) )- -- check that there are in fact devices to provide every required capability- not (any null deviceSets) `holdsOr` Incapable fixI (R.Requirements missingCaps S.empty M.empty) cmd - let minimalInstallSet = smallHittingSet (filter (S.null . S.intersection alreadyInstalled) deviceSets)+ let minimalEquipSet = smallHittingSet (filter (S.null . S.intersection alreadyEquipped) (map fst partitionedDevices)) -- Check that we have enough in our inventory to cover the- -- required installs PLUS what's missing from the child+ -- required devices PLUS what's missing from the child -- inventory. -- What do we need? neededParentInv = missingChildInv- `E.union` (fromList . S.toList $ minimalInstallSet)+ `E.union` (fromList . S.toList $ minimalEquipSet) -- What are we missing? missingParentInv = neededParentInv `E.difference` parentInventory@@ -1639,33 +1881,19 @@ E.isEmpty missingParentInv `holdsOr` Incapable fixI (R.Requirements S.empty S.empty missingMap) cmd - return (minimalInstallSet, missingChildInv)-- -- replace some entity in the world with another entity- changeWorld' ::- Entity ->- V2 Int64 ->- IngredientList Entity ->- W.World Int Entity ->- Either Exn (W.World Int Entity)- changeWorld' eThen loc down w =- let eNow = W.lookupEntity (W.locToCoords loc) w- in if Just eThen /= eNow- then Left $ cmdExn c ["The", eThen ^. entityName, "is not there."]- else- w `updateLoc` loc <$> case down of- [] -> Right Nothing- [de] -> Right $ Just $ snd de- _ -> Left $ Fatal "Bad recipe:\n more than one unmovable entity produced."+ return (minimalEquipSet, missingChildInv) - destroyIfNotBase :: HasRobotStepState sig m => m ()- destroyIfNotBase = do+ destroyIfNotBase :: HasRobotStepState sig m => Maybe GameplayAchievement -> m ()+ destroyIfNotBase mAch = do rid <- use robotID- (rid /= 0) `holdsOrFail` ["You consider destroying your base, but decide not to do it after all."]+ holdsOrFailWithAchievement+ (rid /= 0)+ ["You consider destroying your base, but decide not to do it after all."]+ mAch selfDestruct .= True -- Make sure nothing is in the way. Note that system robots implicitly ignore and base throws on failure.- checkMoveAhead :: HasRobotStepState sig m => V2 Int64 -> MoveFailure -> m ()+ checkMoveAhead :: HasRobotStepState sig m => Location -> MoveFailure -> m () checkMoveAhead nextLoc MoveFailure {..} = do me <- entityAt nextLoc systemRob <- use systemRobot@@ -1674,20 +1902,20 @@ Just e | systemRob -> return () | otherwise -> do- -- robots can not walk through walls- when (e `hasProperty` Unwalkable) $- case failIfBlocked of- Destroy -> destroyIfNotBase- ThrowExn -> throwError $ cmdExn c ["There is a", e ^. entityName, "in the way!"]- IgnoreFail -> return ()+ -- robots can not walk through walls+ when (e `hasProperty` Unwalkable) $+ case failIfBlocked of+ Destroy -> destroyIfNotBase Nothing+ ThrowExn -> throwError $ cmdExn c ["There is a", e ^. entityName, "in the way!"]+ IgnoreFail -> return () - -- robots drown if they walk over liquid without boat- caps <- use robotCapabilities- when (e `hasProperty` Liquid && CFloat `S.notMember` caps) $- case failIfDrown of- Destroy -> destroyIfNotBase- ThrowExn -> throwError $ cmdExn c ["There is a dangerous liquid", e ^. entityName, "in the way!"]- IgnoreFail -> return ()+ -- robots drown if they walk over liquid without boat+ caps <- use robotCapabilities+ when (e `hasProperty` Liquid && CFloat `S.notMember` caps) $+ case failIfDrown of+ Destroy -> destroyIfNotBase Nothing+ ThrowExn -> throwError $ cmdExn c ["There is a dangerous liquid", e ^. entityName, "in the way!"]+ IgnoreFail -> return () getRobotWithinTouch :: HasRobotStepState sig m => RID -> m Robot getRobotWithinTouch rid = do@@ -1704,12 +1932,14 @@ `holdsOrFail` ["The robot with ID", from (show rid), "is not close enough."] return other - -- update some tile in the world setting it to entity or making it empty- updateLoc w loc res = W.update (W.locToCoords loc) (const res) w- holdsOrFail :: (Has (Throw Exn) sig m) => Bool -> [Text] -> m () holdsOrFail a ts = a `holdsOr` cmdExn c ts + holdsOrFailWithAchievement :: (Has (Throw Exn) sig m) => Bool -> [Text] -> Maybe GameplayAchievement -> m ()+ holdsOrFailWithAchievement a ts mAch = case mAch of+ Nothing -> holdsOrFail a ts+ Just ach -> a `holdsOr` cmdExnWithAchievement c ts ach+ isJustOrFail :: (Has (Throw Exn) sig m) => Maybe a -> [Text] -> m a isJustOrFail a ts = a `isJustOr` cmdExn c ts @@ -1808,10 +2038,15 @@ Grab' -> "grabbed" Swap' -> "swapped" +-- | Format a set of suggested devices for use in an error message,+-- in the format @device1 or device2 or ... or deviceN@.+formatDevices :: Set Entity -> Text+formatDevices = T.intercalate " or " . map (^. entityName) . S.toList+ -- | Give some entities from a parent robot (the robot represented by -- the ambient @State Robot@ effect) to a child robot (represented -- by the given 'RID') as part of a 'Build' or 'Reprogram' command.--- The first 'Inventory' is devices to be installed, and the second+-- The first 'Inventory' is devices to be equipped, and the second -- is entities to be transferred. -- -- In classic mode, the entities will be /transferred/ (that is,@@ -1824,15 +2059,15 @@ Inventory -> Inventory -> m ()-provisionChild childID toInstall toGive = do- -- Install and give devices to child- robotMap . ix childID . installedDevices %= E.union toInstall+provisionChild childID toEquip toGive = do+ -- Equip and give devices to child+ robotMap . ix childID . equippedDevices %= E.union toEquip robotMap . ix childID . robotInventory %= E.union toGive -- Delete all items from parent in classic mode creative <- use creativeMode unless creative $- robotInventory %= (`E.difference` (toInstall `E.union` toGive))+ robotInventory %= (`E.difference` (toEquip `E.union` toGive)) -- | Update the location of a robot, and simultaneously update the -- 'robotsByLocation' map, so we can always look up robots by@@ -1840,17 +2075,17 @@ -- of a robot. updateRobotLocation :: (HasRobotStepState sig m) =>- V2 Int64 ->- V2 Int64 ->+ Location ->+ Location -> m () updateRobotLocation oldLoc newLoc | oldLoc == newLoc = return () | otherwise = do- rid <- use robotID- robotsByLocation . at oldLoc %= deleteOne rid- robotsByLocation . at newLoc . non Empty %= IS.insert rid- modify (unsafeSetRobotLocation newLoc)- flagRedraw+ rid <- use robotID+ robotsByLocation . at oldLoc %= deleteOne rid+ robotsByLocation . at newLoc . non Empty %= IS.insert rid+ modify (unsafeSetRobotLocation newLoc)+ flagRedraw where -- Make sure empty sets don't hang around in the -- robotsByLocation map. We don't want a key with an@@ -1943,8 +2178,9 @@ incomparable :: Has (Throw Exn) sig m => Value -> Value -> m a incomparable v1 v2 = throwError $- CmdFailed Lt $- T.unwords ["Comparison is undefined for ", prettyValue v1, "and", prettyValue v2]+ cmdExn+ Lt+ ["Comparison is undefined for ", prettyValue v1, "and", prettyValue v2] ------------------------------------------------------------ -- Arithmetic@@ -1967,13 +2203,13 @@ -- | Perform an integer division, but return @Nothing@ for division by -- zero. safeDiv :: Has (Throw Exn) sig m => Integer -> Integer -> m Integer-safeDiv _ 0 = throwError $ CmdFailed Div "Division by zero"+safeDiv _ 0 = throwError $ cmdExn Div $ pure "Division by zero" safeDiv a b = return $ a `div` b -- | Perform exponentiation, but return @Nothing@ if the power is negative. safeExp :: Has (Throw Exn) sig m => Integer -> Integer -> m Integer safeExp a b- | b < 0 = throwError $ CmdFailed Exp "Negative exponent"+ | b < 0 = throwError $ cmdExn Exp $ pure "Negative exponent" | otherwise = return $ a ^ b ------------------------------------------------------------@@ -2013,7 +2249,7 @@ updateAvailableCommands :: Has (State GameState) sig m => Entity -> m () updateAvailableCommands e = do- let newCaps = S.fromList (e ^. entityCapabilities)+ let newCaps = e ^. entityCapabilities keepConsts = \case Just cap -> cap `S.member` newCaps Nothing -> False
− src/Swarm/Game/Value.hs
@@ -1,117 +0,0 @@-{-# LANGUAGE DeriveAnyClass #-}-{-# LANGUAGE DeriveGeneric #-}-{-# LANGUAGE GADTs #-}---- |--- Module : Swarm.Game.Value--- Copyright : Brent Yorgey--- Maintainer : byorgey@gmail.com------ SPDX-License-Identifier: BSD-3-Clause------ Values and environments used for interpreting the Swarm language.-module Swarm.Game.Value (- -- * Values- Value (..),- prettyValue,- valueToTerm,-- -- * Environments- Env,-) where--import Data.Aeson (FromJSON, ToJSON)-import Data.Bool (bool)-import Data.List (foldl')-import Data.Map qualified as M-import Data.Set qualified as S-import Data.Set.Lens (setOf)-import Data.Text (Text)-import GHC.Generics (Generic)-import Swarm.Language.Context-import Swarm.Language.Pretty (prettyText)-import Swarm.Language.Syntax-import Prelude---- | A /value/ is a term that cannot (or does not) take any more--- evaluation steps on its own.-data Value where- -- | The unit value.- VUnit :: Value- -- | An integer.- VInt :: Integer -> Value- -- | Literal text.- VText :: Text -> Value- -- | A direction.- VDir :: Direction -> Value- -- | A boolean.- VBool :: Bool -> Value- -- | A reference to a robot.- VRobot :: Int -> Value- -- | An injection into a sum type. False = left, True = right.- VInj :: Bool -> Value -> Value- -- | A pair.- VPair :: Value -> Value -> Value- -- | A /closure/, representing a lambda term along with an- -- environment containing bindings for any free variables in the- -- body of the lambda.- VClo :: Var -> Term -> Env -> Value- -- | An application of a constant to some value arguments,- -- potentially waiting for more arguments. If a constant- -- application is fully saturated (as defined by its 'arity'),- -- whether it is a value or not depends on whether or not it- -- represents a command (as defined by 'isCmd'). If a command- -- (e.g. 'Build'), it is a value, and awaits an 'Swarm.Game.CESK.FExec' frame- -- which will cause it to execute. Otherwise (e.g. 'If'), it is- -- not a value, and will immediately reduce.- VCApp :: Const -> [Value] -> Value- -- | A definition, which does not take effect until executed.- -- The @Bool@ indicates whether the definition is recursive.- VDef :: Bool -> Var -> Term -> Env -> Value- -- | The result of a command, consisting of the result of the- -- command as well as an environment of bindings from 'TDef'- -- commands.- VResult :: Value -> Env -> Value- -- | An unevaluated bind expression, waiting to be executed, of the- -- form /i.e./ @c1 ; c2@ or @x <- c1; c2@. We also store an 'Env'- -- in which to interpret the commands.- VBind :: Maybe Var -> Term -> Term -> Env -> Value- -- | A (non-recursive) delayed term, along with its environment. If- -- a term would otherwise be evaluated but we don't want it to be- -- (/e.g./ as in the case of arguments to an 'if', or a recursive- -- binding), we can stick a 'TDelay' on it, which turns it into a- -- value. Delayed terms won't be evaluated until 'Force' is- -- applied to them.- VDelay :: Term -> Env -> Value- -- | A reference to a memory cell in the store.- VRef :: Int -> Value- deriving (Eq, Show, Generic, FromJSON, ToJSON)---- | Pretty-print a value.-prettyValue :: Value -> Text-prettyValue = prettyText . valueToTerm---- | Inject a value back into a term.-valueToTerm :: Value -> Term-valueToTerm VUnit = TUnit-valueToTerm (VInt n) = TInt n-valueToTerm (VText s) = TText s-valueToTerm (VDir d) = TDir d-valueToTerm (VBool b) = TBool b-valueToTerm (VRobot r) = TRobot r-valueToTerm (VInj s v) = TApp (TConst (bool Inl Inr s)) (valueToTerm v)-valueToTerm (VPair v1 v2) = TPair (valueToTerm v1) (valueToTerm v2)-valueToTerm (VClo x t e) =- M.foldrWithKey- (\y v -> TLet False y Nothing (valueToTerm v))- (TLam x Nothing t)- (M.restrictKeys (unCtx e) (S.delete x (setOf fv t)))-valueToTerm (VCApp c vs) = foldl' TApp (TConst c) (reverse (map valueToTerm vs))-valueToTerm (VDef r x t _) = TDef r x Nothing t-valueToTerm (VResult v _) = valueToTerm v-valueToTerm (VBind mx c1 c2 _) = TBind mx c1 c2-valueToTerm (VDelay t _) = TDelay SimpleDelay t-valueToTerm (VRef n) = TRef n---- | An environment is a mapping from variable names to values.-type Env = Ctx Value
src/Swarm/Game/World.hs view
@@ -42,6 +42,9 @@ lookupTerrainM, lookupEntityM, updateM,++ -- ** Runtime updates+ WorldUpdate (..), ) where import Control.Algebra (Has)@@ -53,11 +56,12 @@ import Data.Array.Unboxed qualified as U import Data.Bits import Data.Foldable (foldl')-import Data.Int (Int64)+import Data.Int (Int32) import Data.Map.Strict qualified as M+import Data.Yaml (FromJSON, ToJSON) import GHC.Generics (Generic)-import Linear import Swarm.Util+import Swarm.Util.Location import Prelude hiding (lookup) ------------------------------------------------------------@@ -65,21 +69,25 @@ ------------------------------------------------------------ -- | World coordinates use (row,column) format, with the row--- increasing as we move down the screen. This format plays nicely--- with drawing the screen.-newtype Coords = Coords {unCoords :: (Int64, Int64)}+-- increasing as we move down the screen. We use this format for+-- indexing worlds internally, since it plays nicely with things+-- like drawing the screen, and reading maps from configuration+-- files. The 'locToCoords' and 'coordsToLoc' functions convert back+-- and forth between this type and 'Location', which is used when+-- presenting coordinates externally to the player.+newtype Coords = Coords {unCoords :: (Int32, Int32)} deriving (Eq, Ord, Show, Ix, Generic) instance Rewrapped Coords t instance Wrapped Coords --- | Convert an (x,y) location to a 'Coords' value.-locToCoords :: V2 Int64 -> Coords-locToCoords (V2 x y) = Coords (-y, x)+-- | Convert an external (x,y) location to an internal 'Coords' value.+locToCoords :: Location -> Coords+locToCoords (Location x y) = Coords (-y, x) --- | Convert 'Coords' to an (x,y) location.-coordsToLoc :: Coords -> V2 Int64-coordsToLoc (Coords (r, c)) = V2 c (-r)+-- | Convert an internal 'Coords' value to an external (x,y) location.+coordsToLoc :: Coords -> Location+coordsToLoc (Coords (r, c)) = Location c (-r) ------------------------------------------------------------ -- World function@@ -95,7 +103,7 @@ -- | Create a world function from a finite array of specified cells -- plus a single default cell to use everywhere else.-worldFunFromArray :: Array (Int64, Int64) (t, Maybe e) -> (t, Maybe e) -> WorldFun t e+worldFunFromArray :: Array (Int32, Int32) (t, Maybe e) -> (t, Maybe e) -> WorldFun t e worldFunFromArray arr def = WF $ \(Coords (r, c)) -> if inRange bnds (r, c) then arr ! (r, c)@@ -120,7 +128,7 @@ -- | The number consisting of 'tileBits' many 1 bits. We can use this -- to mask out the tile offset of a coordinate.-tileMask :: Int64+tileMask :: Int32 tileMask = (1 `shiftL` tileBits) - 1 -- | If we think of the world as a grid of /tiles/, we can assign each@@ -274,3 +282,18 @@ where tileCorner = tileOrigin tc (terrain, entities) = unzip $ map (runWF f . plusOffset tileCorner) (range tileBounds)++-- ------------------------------------------------------------------+-- Runtime world update+-- ------------------------------------------------------------------++-- | Update world in an inspectable way.+--+-- This type is used for changes by e.g. the drill command at later+-- tick. Using ADT allows us to serialize and inspect the updates.+data WorldUpdate e = ReplaceEntity+ { updatedLoc :: Location+ , originalEntity :: e+ , newEntity :: Maybe e+ }+ deriving (Eq, Ord, Show, Generic, FromJSON, ToJSON)
src/Swarm/Game/WorldGen.hs view
@@ -18,7 +18,7 @@ import Data.ByteString (ByteString) import Data.Enumeration import Data.Hash.Murmur-import Data.Int (Int64)+import Data.Int (Int32) import Data.List (find) import Data.Maybe (fromMaybe, mapMaybe) import Data.Set qualified as S@@ -102,9 +102,9 @@ | sample ix cl0 > 0.5 = (StoneT, Just "mountain") | h `mod` 30 == 0 = (StoneT, Just "boulder") | sample ix cl0 > 0 =- case h `mod` 30 of- 1 -> (DirtT, Just "LaTeX")- _ -> (DirtT, Just "tree")+ case h `mod` 30 of+ 1 -> (DirtT, Just "LaTeX")+ _ -> (DirtT, Just "tree") | otherwise = (GrassT, Nothing) genBiome Small Hard Natural | h `mod` 100 == 0 = (StoneT, Just "lodestone")@@ -153,7 +153,7 @@ -- | Create a world function from a finite array of specified cells -- plus a seed to randomly generate the rest.-testWorld2FromArray :: EntityMap -> Array (Int64, Int64) (TerrainType, Maybe Entity) -> Seed -> WorldFun TerrainType Entity+testWorld2FromArray :: EntityMap -> Array (Int32, Int32) (TerrainType, Maybe Entity) -> Seed -> WorldFun TerrainType Entity testWorld2FromArray em arr seed = WF $ \co@(Coords (r, c)) -> if inRange bnds (r, c) then arr ! (r, c)@@ -167,7 +167,7 @@ findOffset :: Integer -> ((Coords -> (t, Maybe e)) -> Bool) -> WorldFun t e -> WorldFun t e findOffset skip isGood (WF f) = WF f' where- offset :: Enumeration Int64+ offset :: Enumeration Int32 offset = fromIntegral . (skip *) <$> int f' =
src/Swarm/Language/Capability.hs view
@@ -8,7 +8,7 @@ -- Capabilities needed to evaluate and execute programs. Language -- constructs or commands require certain capabilities, and in turn -- capabilities are provided by various devices. A robot must have an--- appropriate device installed in order to make use of each language+-- appropriate device equipped in order to make use of each language -- construct or command. module Swarm.Language.Capability ( Capability (..),@@ -51,8 +51,10 @@ CPlace | -- | Execute the 'Give' command CGive- | -- | Execute the 'Install' command- CInstall+ | -- | Execute the 'Equip' command+ CEquip+ | -- | Execute the 'Unequip' command+ CUnequip | -- | Execute the 'Make' command CMake | -- | Execute the 'Count' command@@ -67,7 +69,7 @@ CSenseloc | -- | Execute the 'Blocked' command CSensefront- | -- | Execute the 'Ishere' command+ | -- | Execute the 'Ishere' and 'Isempty' commands CSensehere | -- | Execute the 'Scan' command CScan@@ -83,6 +85,8 @@ CLog | -- | Manipulate text values CText+ | -- | Convert between characters/text and Unicode values+ CCode | -- | Don't drown in liquid CFloat | -- | Evaluate conditional expressions@@ -103,6 +107,8 @@ CRecursion | -- | Execute the 'Reprogram' command CReprogram+ | -- | Execute the `meet` and `meetAll` commands.+ CMeet | -- | Capability to introspect and see its own name CWhoami | -- | Capability to set its own name@@ -157,7 +163,7 @@ Undefined -> Nothing Fail -> Nothing Has -> Nothing- Installed -> Nothing+ Equipped -> Nothing -- speaking is natural to robots (unlike listening) Say -> Nothing -- TODO: #495@@ -175,17 +181,21 @@ Harvest -> Just CHarvest Place -> Just CPlace Give -> Just CGive- Install -> Just CInstall+ Equip -> Just CEquip+ Unequip -> Just CUnequip Make -> Just CMake Count -> Just CCount If -> Just CCond Blocked -> Just CSensefront Scan -> Just CScan Ishere -> Just CSensehere+ Isempty -> Just CSensehere Upload -> Just CScan Build -> Just CBuild Salvage -> Just CSalvage Reprogram -> Just CReprogram+ Meet -> Just CMeet+ MeetAll -> Just CMeet Drill -> Just CDrill Neg -> Just CArith Add -> Just CArith@@ -199,12 +209,16 @@ Atomic -> Just CAtomic Time -> Just CTime Wait -> Just CTime+ Whereami -> Just CSenseloc+ Heading -> Just COrient -- ---------------------------------------------------------------- -- Text operations Format -> Just CText Concat -> Just CText Split -> Just CText Chars -> Just CText+ CharAt -> Just CCode+ ToChar -> Just CCode -- ---------------------------------------------------------------- -- Some God-like abilities. As -> Just CGod@@ -234,7 +248,7 @@ Case -> Just CSum Fst -> Just CProd Snd -> Just CProd- -- XXX pair syntax should require CProd too+ -- TODO: #563 pair syntax (1,2,3...) should require CProd too -- ---------------------------------------------------------------- -- Some additional straightforward ones, which however currently@@ -242,10 +256,9 @@ -- which conveys their capability. TODO: #26 Teleport -> Just CTeleport -- Some space-time machine like Tardis? Appear -> Just CAppear -- paint?- Whereami -> Just CSenseloc -- GPS? Random -> Just CRandom -- randomness device (with bitcoins)? -- ---------------------------------------------------------------- -- Some more constants which *ought* to have their own capability but- -- currently don't. TODO: #26- View -> Nothing -- XXX this should also require something.+ -- currently don't.+ View -> Nothing -- TODO: #17 should require equipping an antenna Knows -> Nothing
src/Swarm/Language/Context.hs view
@@ -1,4 +1,3 @@-{-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE DeriveDataTypeable #-} -- |
src/Swarm/Language/Elaborate.hs view
@@ -1,3 +1,5 @@+{-# LANGUAGE OverloadedStrings #-}+ -- | -- Module : Swarm.Language.Elaborate -- Copyright : Brent Yorgey@@ -8,40 +10,51 @@ -- Term elaboration which happens after type checking. module Swarm.Language.Elaborate where -import Control.Lens (transform, (%~))+import Control.Lens (transform, (%~), (^.)) import Swarm.Language.Syntax+import Swarm.Language.Types -- | Perform some elaboration / rewriting on a fully type-annotated--- term, given its top-level type. This currently performs such--- operations as rewriting @if@ expressions and recursive let--- expressions to use laziness appropriately. In theory it could--- also perform rewriting for overloaded constants depending on the--- actual type they are used at, but currently that sort of thing--- tends to make type inference fall over.-elaborate :: Term -> Term+-- term. This currently performs such operations as rewriting @if@+-- expressions and recursive let expressions to use laziness+-- appropriately. In theory it could also perform rewriting for+-- overloaded constants depending on the actual type they are used+-- at, but currently that sort of thing tends to make type inference+-- fall over.+elaborate :: Syntax' Polytype -> Syntax' Polytype elaborate = -- Wrap all *free* variables in 'Force'. Free variables must be -- referring to a previous definition, which are all wrapped in -- 'TDelay'.- (fvT %~ TApp (TConst Force))+ (freeVarsS %~ \s -> Syntax' (s ^. sLoc) (SApp sForce s) (s ^. sType)) -- Now do additional rewriting on all subterms. . transform rewrite where- -- For recursive let bindings, rewrite any occurrences of x to- -- (force x). When interpreting t1, we will put a binding (x |->- -- delay t1) in the context.- rewrite (TLet True x ty t1 t2) = TLet True x ty (wrapForce x t1) (wrapForce x t2)- -- Rewrite any recursive occurrences of x inside t1 to (force x).- -- When a TDef is encountered at runtime its body will immediately- -- be wrapped in a VDelay. However, to make this work we also need- -- to wrap all free variables in any term with 'force' --- since- -- any such variables must in fact refer to things previously- -- bound by 'def'.- rewrite (TDef True x ty t1) = TDef True x ty (mapFree1 x (TApp (TConst Force)) t1)- -- Rewrite @f $ x@ to @f x@.- rewrite (TApp (TApp (TConst AppF) r) l) = TApp r l- -- Leave any other subterms alone.- rewrite t = t+ rewrite :: Syntax' Polytype -> Syntax' Polytype+ rewrite (Syntax' l t ty) = Syntax' l (rewriteTerm t) ty -wrapForce :: Var -> Term -> Term-wrapForce x = mapFree1 x (TApp (TConst Force))+ rewriteTerm :: Term' Polytype -> Term' Polytype+ rewriteTerm = \case+ -- For recursive let bindings, rewrite any occurrences of x to+ -- (force x). When interpreting t1, we will put a binding (x |->+ -- delay t1) in the context.+ SLet True x ty t1 t2 -> SLet True x ty (wrapForce (lvVar x) t1) (wrapForce (lvVar x) t2)+ -- Rewrite any recursive occurrences of x inside t1 to (force x).+ -- When a TDef is encountered at runtime its body will immediately+ -- be wrapped in a VDelay. However, to make this work we also need+ -- to wrap all free variables in any term with 'force' --- since+ -- any such variables must in fact refer to things previously+ -- bound by 'def'.+ SDef True x ty t1 -> SDef True x ty (wrapForce (lvVar x) t1)+ -- Rewrite @f $ x@ to @f x@.+ SApp (Syntax' _ (SApp (Syntax' _ (TConst AppF) _) l) _) r -> SApp l r+ -- Leave any other subterms alone.+ t -> t++wrapForce :: Var -> Syntax' Polytype -> Syntax' Polytype+wrapForce x = mapFreeS x (\s@(Syntax' l _ ty) -> Syntax' l (SApp sForce s) ty)++-- Note, TyUnit is not the right type, but I don't want to bother++sForce :: Syntax' Polytype+sForce = Syntax' NoLoc (TConst Force) (Forall ["a"] (TyDelay (TyVar "a") :->: TyVar "a"))
src/Swarm/Language/LSP.hs view
@@ -14,15 +14,17 @@ import Control.Lens (to, (^.)) import Control.Monad (void) import Control.Monad.IO.Class-import Data.Foldable (forM_)-import Data.Maybe (fromMaybe)+import Data.Maybe (fromMaybe, mapMaybe) import Data.Text (Text) import Data.Text.IO qualified as Text import Language.LSP.Diagnostics import Language.LSP.Server+import Language.LSP.Types (Hover (Hover)) import Language.LSP.Types qualified as J import Language.LSP.Types.Lens qualified as J import Language.LSP.VFS+import Swarm.Language.LSP.Hover qualified as H+import Swarm.Language.LSP.VarUsage qualified as VU import Swarm.Language.Parse import Swarm.Language.Pipeline import System.IO (stderr)@@ -40,7 +42,7 @@ , interpretHandler = \env -> Iso (runLspT env) liftIO , options = defaultOptions- { -- set sync options to get DidSave event+ { -- set sync options to get DidSave event, as well as Open and Close events. textDocumentSync = Just ( J.TextDocumentSyncOptions@@ -58,39 +60,75 @@ -- handler is called for each key-stroke. syncKind = J.TdSyncFull +diagnosticSourcePrefix :: Text+diagnosticSourcePrefix = "swarm-lsp"+ debug :: MonadIO m => Text -> m () debug msg = liftIO $ Text.hPutStrLn stderr $ "[swarm-lsp] " <> msg validateSwarmCode :: J.NormalizedUri -> J.TextDocumentVersion -> Text -> LspM () () validateSwarmCode doc version content = do -- debug $ "Validating: " <> from (show doc) <> " ( " <> content <> ")"- flushDiagnosticsBySource 0 (Just "swarm-lsp")- let err = case readTerm' content of- Right Nothing -> Nothing- Right (Just term) -> case processParsedTerm' mempty mempty term of- Right _ -> Nothing- Left e -> Just $ showTypeErrorPos content e- Left e -> Just $ showErrorPos e++ -- FIXME: #1040 With this call to flushDiagnosticsBySource in place, the warnings+ -- in other buffers (editor tabs) end up getting cleared when switching between+ -- (focusing on) other buffers in VS Code.+ -- However, getting rid of this seems to break error highlighting.+ flushDiagnosticsBySource 0 (Just diagnosticSourcePrefix)++ let (parsingErrs, unusedVarWarnings) = case readTerm' content of+ Right Nothing -> ([], [])+ Right (Just term) -> (parsingErrors, unusedWarnings)+ where+ VU.Usage _ problems = VU.getUsage mempty term+ unusedWarnings = mapMaybe (VU.toErrPos content) problems++ parsingErrors = case processParsedTerm' mempty mempty term of+ Right _ -> []+ Left e -> pure $ showTypeErrorPos content e+ Left e -> (pure $ showErrorPos e, []) -- debug $ "-> " <> from (show err)- forM_ err sendDiagnostic++ publishDiags $+ map makeUnusedVarDiagnostic unusedVarWarnings++ -- NOTE: "publishDiags" keeps only one diagnostic at a+ -- time (the most recent) so we make sure the errors are+ -- issued last (after any warnings).+ -- Note that it does not achieve the desired effect to simply+ -- concatenate the two diagnostic lists into a single+ -- publishDiagnostics function call (regardless of the order+ -- of the lists).+ publishDiags $+ map makeParseErrorDiagnostic parsingErrs where- sendDiagnostic :: ((Int, Int), (Int, Int), Text) -> LspM () ()- sendDiagnostic ((startLine, startCol), (endLine, endCol), msg) = do- let diags =- [ J.Diagnostic- ( J.Range- (J.Position (fromIntegral startLine) (fromIntegral startCol))- (J.Position (fromIntegral endLine) (fromIntegral endCol))- )- (Just J.DsWarning) -- severity- Nothing -- code- (Just "swarm-lsp") -- source- msg- Nothing -- tags- (Just (J.List []))- ]- publishDiagnostics 1 doc version (partitionBySource diags)+ publishDiags :: [J.Diagnostic] -> LspM () ()+ publishDiags = publishDiagnostics 1 doc version . partitionBySource + makeUnusedVarDiagnostic :: (J.Range, Text) -> J.Diagnostic+ makeUnusedVarDiagnostic (range, msg) =+ J.Diagnostic+ range+ (Just J.DsWarning) -- severity+ Nothing -- code+ (Just diagnosticSourcePrefix) -- source+ msg+ (Just (J.List [J.DtUnnecessary])) -- tags+ Nothing -- related source code info+ makeParseErrorDiagnostic :: ((Int, Int), (Int, Int), Text) -> J.Diagnostic+ makeParseErrorDiagnostic ((startLine, startCol), (endLine, endCol), msg) =+ J.Diagnostic+ ( J.Range+ (J.Position (fromIntegral startLine) (fromIntegral startCol))+ (J.Position (fromIntegral endLine) (fromIntegral endCol))+ )+ (Just J.DsError) -- severity+ Nothing -- code+ (Just diagnosticSourcePrefix) -- source+ msg+ Nothing -- tags+ (Just (J.List []))+ handlers :: Handlers (LspM ()) handlers = mconcat@@ -111,4 +149,13 @@ Just vf@(VirtualFile _ version _rope) -> do validateSwarmCode doc (Just $ fromIntegral version) (virtualFileText vf) _ -> debug $ "No virtual file found for: " <> from (show msg)+ , requestHandler J.STextDocumentHover $ \req responder -> do+ let doc = req ^. J.params . J.textDocument . J.uri . to J.toNormalizedUri+ pos = req ^. J.params . J.position+ mdoc <- getVirtualFile doc+ let maybeHover = do+ vf <- mdoc+ (markdownText, maybeRange) <- H.showHoverInfo doc Nothing pos vf+ return $ Hover (J.HoverContents $ J.MarkupContent J.MkMarkdown markdownText) maybeRange+ responder $ Right maybeHover ]
+ src/Swarm/Language/LSP/Hover.hs view
@@ -0,0 +1,260 @@+{-# LANGUAGE OverloadedStrings #-}++module Swarm.Language.LSP.Hover (+ showHoverInfo,++ -- * Documentation rendering+ renderDoc,+ treeToMarkdown,++ -- * Finding source location+ narrowToPosition,++ -- * Explaining source position+ explain,+) where++import Control.Applicative ((<|>))+import Control.Lens ((^.))+import Control.Monad (guard, void)+import Data.Graph+import Data.List.NonEmpty (NonEmpty (..))+import Data.Maybe (fromMaybe)+import Data.Text (Text)+import Data.Text qualified as T+import Data.Text.Utf16.Rope qualified as R+import Language.LSP.Types qualified as J+import Language.LSP.VFS+import Swarm.Language.Context as Ctx+import Swarm.Language.Module (Module (..))+import Swarm.Language.Parse (readTerm', unTuple)+import Swarm.Language.Pipeline (ProcessedTerm (..), processParsedTerm)+import Swarm.Language.Pretty (prettyText)+import Swarm.Language.Syntax+import Swarm.Language.Typecheck (inferConst)+import Swarm.Language.Types+import Swarm.Util qualified as U++withinBound :: Int -> SrcLoc -> Bool+withinBound pos (SrcLoc s e) = pos >= s && pos < e+withinBound _ NoLoc = False++ropeToLspPosition :: R.Position -> J.Position+ropeToLspPosition (R.Position l c) =+ J.Position (fromIntegral l) (fromIntegral c)++lspToRopePosition :: J.Position -> R.Position+lspToRopePosition (J.Position myLine myCol) =+ R.Position (fromIntegral myLine) (fromIntegral myCol)++showHoverInfo ::+ J.NormalizedUri ->+ J.TextDocumentVersion ->+ J.Position ->+ VirtualFile ->+ Maybe (Text, Maybe J.Range)+showHoverInfo _ _ p vf@(VirtualFile _ _ myRope) =+ case readTerm' content of+ Left _ -> Nothing+ Right Nothing -> Nothing+ Right (Just stx) -> Just $ case processParsedTerm stx of+ Left _e ->+ let found@(Syntax foundSloc _) = narrowToPosition stx $ fromIntegral absolutePos+ finalPos = posToRange myRope foundSloc+ in (,finalPos) . treeToMarkdown 0 $ explain found+ Right (ProcessedTerm modul _req _reqCtx) ->+ let found@(Syntax' foundSloc _ _) = narrowToPosition (moduleAST modul) $ fromIntegral absolutePos+ finalPos = posToRange myRope foundSloc+ in (,finalPos) . treeToMarkdown 0 $ explain found+ where+ content = virtualFileText vf+ absolutePos =+ maybe 0 (R.length . fst) $+ R.splitAtPosition (lspToRopePosition p) myRope++posToRange :: R.Rope -> SrcLoc -> Maybe J.Range+posToRange myRope foundSloc = do+ (s, e) <- case foundSloc of+ SrcLoc s e -> Just (s, e)+ _ -> Nothing+ (startRope, _) <- R.splitAt (fromIntegral s) myRope+ (endRope, _) <- R.splitAt (fromIntegral e) myRope+ return $+ J.Range+ (ropeToLspPosition $ R.lengthAsPosition startRope)+ (ropeToLspPosition $ R.lengthAsPosition endRope)++descend ::+ ExplainableType ty =>+ -- | position+ Int ->+ -- | next element to inspect+ Syntax' ty ->+ Maybe (Syntax' ty)+descend pos s1@(Syntax' l1 _ _) = do+ guard $ withinBound pos l1+ return $ narrowToPosition s1 pos++-- | Find the most specific term for a given+-- position within the code.+narrowToPosition ::+ ExplainableType ty =>+ -- | parent term+ Syntax' ty ->+ -- | absolute offset within the file+ Int ->+ Syntax' ty+narrowToPosition s0@(Syntax' _ t ty) pos = fromMaybe s0 $ case t of+ SLam lv _ s -> d (locVarToSyntax' lv $ getInnerType ty) <|> d s+ SApp s1 s2 -> d s1 <|> d s2+ SLet _ lv _ s1@(Syntax' _ _ lty) s2 -> d (locVarToSyntax' lv lty) <|> d s1 <|> d s2+ SDef _ lv _ s@(Syntax' _ _ lty) -> d (locVarToSyntax' lv lty) <|> d s+ SBind mlv s1@(Syntax' _ _ lty) s2 -> (mlv >>= d . flip locVarToSyntax' (getInnerType lty)) <|> d s1 <|> d s2+ SPair s1 s2 -> d s1 <|> d s2+ SDelay _ s -> d s+ -- atoms - return their position and end recursion+ TUnit -> Nothing+ TConst {} -> Nothing+ TDir {} -> Nothing+ TInt {} -> Nothing+ TText {} -> Nothing+ TBool {} -> Nothing+ TVar {} -> Nothing+ TRequire {} -> Nothing+ TRequireDevice {} -> Nothing+ -- these should not show up in surface language+ TRef {} -> Nothing+ TRobot {} -> Nothing+ TAntiInt {} -> Nothing+ TAntiText {} -> Nothing+ where+ d = descend pos++renderDoc :: Int -> Text -> Text+renderDoc d t+ | d == 0 = t+ | otherwise = T.drop 2 . indent (max 0 (4 * (d - 1)) + 2) $ "* " <> t+ where+ indent x = T.unlines . map (T.replicate x " " <>) . T.lines++treeToMarkdown :: Int -> Tree Text -> Text+treeToMarkdown d (Node t children) =+ T.unlines $ renderDoc d t : map (treeToMarkdown $ d + 1) children++class Show t => ExplainableType t where+ -- | Pretty print the type.+ prettyType :: t -> Text++ -- | Strip the type of its outermost layer.+ --+ -- This allows us to strip lambda or command type+ -- and get the type of the bound variable.+ getInnerType :: t -> t++ -- | Check if this type is same as the given 'Polytype'.+ --+ -- We use it to not print same type twice (e.g. inferred and generic).+ eq :: t -> Polytype -> Bool++instance ExplainableType () where+ prettyType = const "?"+ getInnerType = id+ eq _ _ = False++instance ExplainableType Polytype where+ prettyType = prettyText+ getInnerType = fmap $ \case+ (l :->: _r) -> l+ (TyCmd t) -> t+ t -> t+ eq = (==)++explain :: ExplainableType ty => Syntax' ty -> Tree Text+explain trm = case trm ^. sTerm of+ TUnit -> literal "The unit value."+ TConst c -> literal . constGenSig c $ briefDoc (constDoc $ constInfo c)+ TDir {} -> literal "A direction literal."+ TInt {} -> literal "An integer literal."+ TText {} -> literal "A text literal."+ TBool {} -> literal "A boolean literal."+ TVar v -> pure $ typeSignature v ty ""+ -- special forms (function application will show for `$`, but really should be rare)+ SApp {} -> explainFunction trm+ TRequireDevice {} -> pure "Require a specific device to be equipped."+ TRequire {} -> pure "Require a certain number of an entity."+ -- definition or bindings+ SLet isRecursive var mTypeAnn rhs _b -> pure $ explainDefinition False isRecursive var (rhs ^. sType) mTypeAnn+ SDef isRecursive var mTypeAnn rhs -> pure $ explainDefinition True isRecursive var (rhs ^. sType) mTypeAnn+ SLam (LV _s v) _mType _syn ->+ pure $+ typeSignature v ty $+ "A lambda expression binding the variable " <> U.bquote v <> "."+ SBind mv rhs _cmds ->+ pure $+ typeSignature (maybe "__rhs" lvVar mv) (getInnerType $ rhs ^. sType) $+ "A monadic bind for commands" <> maybe "." (\(LV _s v) -> ", that binds variable " <> U.bquote v <> ".") mv+ -- composite types+ SPair {} ->+ Node+ (typeSignature "_" ty "A tuple consisting of:")+ (explain <$> unTuple trm)+ SDelay {} ->+ pure . T.unlines $+ [ "Delay evaluation of a term, written `{...}`."+ , ""+ , "Swarm is an eager language, but in some cases (e.g. for `if` statements and recursive bindings) we need to delay evaluation."+ , ""+ , "The counterpart to `{...}` is `force`:"+ , "```"+ , "force {t} = t"+ , "```"+ ]+ -- internal syntax that should not actually show in hover+ TRef {} -> internal "A memory reference."+ TAntiInt {} -> internal "An antiquoted Haskell variable name of type Integer."+ TAntiText {} -> internal "An antiquoted Haskell variable name of type Text."+ TRobot {} -> internal "A robot reference."+ where+ ty = trm ^. sType+ literal = pure . typeSignature (prettyText . void $ trm ^. sTerm) ty+ internal description = literal $ description <> "\n**These should never show up in surface syntax.**"+ constGenSig c =+ let ity = inferConst c+ in if ty `eq` ity then id else typeSignature (prettyText c) ity++-- | Helper function to explain function application.+--+-- Note that 'Force' is often inserted internally, so+-- if it shows up here we drop it.+explainFunction :: ExplainableType ty => Syntax' ty -> Tree Text+explainFunction s =+ case unfoldApps s of+ (Syntax' _ (TConst Force) _ :| [innerT]) -> explain innerT+ (Syntax' _ (TConst Force) _ :| f : params) -> explainF f params+ (f :| params) -> explainF f params+ where+ explainF f params =+ Node+ "Function application of:"+ [ explain f+ , Node+ "with parameters:"+ (map explain params)+ ]++explainDefinition :: ExplainableType ty => Bool -> Bool -> LocVar -> ty -> Maybe Polytype -> Text+explainDefinition isDef isRecursive (LV _s var) ty maybeTypeAnnotation =+ typeSignature var ty $+ T.unwords+ [ "A"+ , (if isRecursive then "" else "non-") <> "recursive"+ , if isDef then "definition" else "let"+ , "expression"+ , if null maybeTypeAnnotation then "without" else "with"+ , "a type annotation on the variable."+ ]++typeSignature :: ExplainableType ty => Var -> ty -> Text -> Text+typeSignature v typ body = T.unlines ["```", short, "```", body]+ where+ short = v <> ": " <> prettyType typ
+ src/Swarm/Language/LSP/VarUsage.hs view
@@ -0,0 +1,108 @@+{-# LANGUAGE OverloadedStrings #-}++module Swarm.Language.LSP.VarUsage where++import Control.Monad (guard)+import Data.List.NonEmpty (NonEmpty (..))+import Data.Map (Map)+import Data.Map qualified as M+import Data.Set (Set)+import Data.Set qualified as S+import Data.Text (Text)+import Data.Text qualified as T+import Language.LSP.Types qualified as J+import Swarm.Language.Parse qualified as P+import Swarm.Language.Syntax+import Swarm.Util qualified as U++data BindingType+ = Lambda+ | Let+ | Bind+ deriving (Eq, Show)++data VarUsage = VarUsage LocVar BindingType++type BindingSites = Map Var (NonEmpty SrcLoc)++data Usage = Usage+ { usages :: Set LocVar+ -- ^ Variable references+ , problems :: [VarUsage]+ -- ^ Variable declarations without any references+ }++instance Semigroup Usage where+ Usage y1 z1 <> Usage y2 z2 =+ Usage+ (y1 <> y2)+ (z1 <> z2)++instance Monoid Usage where+ mempty = Usage mempty mempty++toErrPos :: Text -> VarUsage -> Maybe (J.Range, Text)+toErrPos code (VarUsage (LV loc v) scope) = do+ -- A leading underscore will suppress the unused variable warning+ guard $ not $ "_" `T.isPrefixOf` v+ rangePair <- case loc of+ SrcLoc s e -> Just (s, e)+ _ -> Nothing+ let (start, end) = P.getLocRange code rangePair+ ((startLine, startCol), (endLine, endCol)) = (minusOne start, minusOne end)+ range =+ J.Range+ (J.Position (fromIntegral startLine) (fromIntegral startCol))+ (J.Position (fromIntegral endLine) (fromIntegral endCol))+ return (range, txt)+ where+ txt =+ T.unwords+ [ "Unused variable"+ , U.quote v+ , "in"+ , T.pack $ show scope+ , "expression"+ ]+ minusOne (x, y) = (x - 1, y - 1)++-- | Descends the syntax tree rooted at a variable declaration,+-- accumulating variable references.+-- Generates a "problem" if an associated variable reference+-- is not encountered in the subtree for this declaration.+checkOccurrences ::+ BindingSites ->+ LocVar ->+ BindingType ->+ [Syntax] ->+ Usage+checkOccurrences bindings lv@(LV loc v) declType childSyntaxes =+ Usage childUsages $ missing <> deeperMissing+ where+ deeperBindings = M.insertWith (<>) v (pure loc) bindings+ Usage childUsages deeperMissing = mconcat $ map (getUsage deeperBindings) childSyntaxes+ missing = [VarUsage lv declType | lv `S.notMember` childUsages]++-- | Build up the bindings map as a function argument as+-- we descend into the syntax tree.+-- Aggregates unused bindings as we return from each layer.+getUsage ::+ BindingSites ->+ Syntax ->+ Usage+getUsage bindings (Syntax _pos t) = case t of+ TVar v -> Usage myUsages mempty+ where+ myUsages = case M.lookup v bindings of+ Nothing -> mempty+ Just (loc :| _) -> S.singleton $ LV loc v+ SLam v _ s -> checkOccurrences bindings v Lambda [s]+ SApp s1 s2 -> getUsage bindings s1 <> getUsage bindings s2+ SLet _ v _ s1 s2 -> getUsage bindings s1 <> checkOccurrences bindings v Let [s2]+ SPair s1 s2 -> getUsage bindings s1 <> getUsage bindings s2+ SDef _ _v _ s -> getUsage bindings s+ SBind maybeVar s1 s2 -> case maybeVar of+ Just v -> checkOccurrences bindings v Bind [s1, s2]+ Nothing -> getUsage bindings s1 <> getUsage bindings s2+ SDelay _ s -> getUsage bindings s+ _ -> mempty
+ src/Swarm/Language/Module.hs view
@@ -0,0 +1,51 @@+-- |+-- Module : Swarm.Language.Module+-- Copyright : Brent Yorgey+-- Maintainer : byorgey@gmail.com+--+-- SPDX-License-Identifier: BSD-3-Clause+--+-- A 'Module' packages together a type-annotated syntax tree along+-- with a context of top-level definitions.+module Swarm.Language.Module (+ -- * Modules+ Module (..),+ TModule,+ UModule,+ trivMod,+) where++import Data.Data (Data)+import Data.Yaml (FromJSON, ToJSON)+import GHC.Generics (Generic)+import Swarm.Language.Context (Ctx, empty)+import Swarm.Language.Syntax (Syntax')+import Swarm.Language.Types (Polytype, UPolytype, UType)++------------------------------------------------------------+-- Modules+------------------------------------------------------------++-- | A module generally represents the result of performing type+-- inference on a top-level expression, which in particular can+-- contain definitions ('Swarm.Language.Syntax.TDef'). A module+-- contains the type-annotated AST of the expression itself, as well+-- as the context giving the types of any defined variables.+data Module s t = Module {moduleAST :: Syntax' s, moduleCtx :: Ctx t}+ deriving (Show, Eq, Functor, Data, Generic, FromJSON, ToJSON)++-- | A 'TModule' is the final result of the type inference process on+-- an expression: we get a polytype for the expression, and a+-- context of polytypes for the defined variables.+type TModule = Module Polytype Polytype++-- | A 'UModule' represents the type of an expression at some+-- intermediate stage during the type inference process. We get a+-- 'UType' (/not/ a 'UPolytype') for the expression, which may+-- contain some free unification or type variables, as well as a+-- context of 'UPolytype's for any defined variables.+type UModule = Module UType UPolytype++-- | The trivial module for a given AST, with the empty context.+trivMod :: Syntax' s -> Module s t+trivMod t = Module t empty
src/Swarm/Language/Parse.hs view
@@ -33,8 +33,10 @@ showShortError, showErrorPos, getLocRange,+ unTuple, ) where +import Control.Lens (view, (^.)) import Control.Monad.Combinators.Expr import Control.Monad.Reader import Data.Bifunctor@@ -87,7 +89,7 @@ , "text" , "dir" , "bool"- , "robot"+ , "actor" , "cmd" , "delay" , "let"@@ -128,13 +130,17 @@ -- | Parse an identifier, i.e. any non-reserved string containing -- alphanumeric characters and underscores and not starting with a -- number.-identifier :: Parser Text-identifier = (lexeme . try) (p >>= check) <?> "variable name"+identifier :: Parser Var+identifier = lvVar <$> locIdentifier++-- | Parse an identifier together with its source location info.+locIdentifier :: Parser LocVar+locIdentifier = uncurry LV <$> parseLocG ((lexeme . try) (p >>= check) <?> "variable name") where p = (:) <$> (letterChar <|> char '_') <*> many (alphaNumChar <|> char '_' <|> char '\'') check s | toLower t `elem` reservedWords =- fail $ "reserved word '" ++ s ++ "' cannot be used as variable name"+ fail $ "reserved word '" ++ s ++ "' cannot be used as variable name" | otherwise = return t where t = into @Text s@@ -185,11 +191,11 @@ -- Otherwise, require all variables to be explicitly quantified | S.null free = return $ Forall xs ty | otherwise =- fail $- unlines- [ " Type contains free variable(s): " ++ unwords (map from (S.toList free))- , " Try adding them to the 'forall'."- ]+ fail $+ unlines+ [ " Type contains free variable(s): " ++ unwords (map from (S.toList free))+ , " Try adding them to the 'forall'."+ ] where free = tyVars ty `S.difference` S.fromList xs @@ -212,7 +218,7 @@ <|> TyText <$ reserved "text" <|> TyDir <$ reserved "dir" <|> TyBool <$ reserved "bool"- <|> TyRobot <$ reserved "robot"+ <|> TyActor <$ reserved "actor" <|> TyCmd <$> (reserved "cmd" *> parseTypeAtom) <|> TyDelay <$> braces parseType <|> parens parseType@@ -229,15 +235,15 @@ consts = filter isUserFunc allConst alternative c = c <$ reserved (syntax $ constInfo c) --- | Add 'Location' to a parser-parseLocG :: Parser a -> Parser (Location, a)+-- | Add 'SrcLoc' to a parser+parseLocG :: Parser a -> Parser (SrcLoc, a) parseLocG pa = do start <- getOffset a <- pa end <- getOffset- pure (Location start end, a)+ pure (SrcLoc start end, a) --- | Add 'Location' to a 'Term' parser+-- | Add 'SrcLoc' to a 'Term' parser parseLoc :: Parser Term -> Parser Syntax parseLoc pterm = uncurry Syntax <$> parseLocG pterm @@ -255,21 +261,25 @@ *> ( ( TRequireDevice <$> (textLiteral <?> "device name in double quotes") )- <|> ( TRequire <$> (fromIntegral <$> integer)+ <|> ( TRequire+ <$> (fromIntegral <$> integer) <*> (textLiteral <?> "entity name in double quotes") ) )- <|> SLam <$> (symbol "\\" *> identifier)+ <|> SLam+ <$> (symbol "\\" *> locIdentifier) <*> optional (symbol ":" *> parseType) <*> (symbol "." *> parseTerm)- <|> sLet <$> (reserved "let" *> identifier)+ <|> sLet+ <$> (reserved "let" *> locIdentifier) <*> optional (symbol ":" *> parsePolytype) <*> (symbol "=" *> parseTerm) <*> (reserved "in" *> parseTerm)- <|> sDef <$> (reserved "def" *> identifier)+ <|> sDef+ <$> (reserved "def" *> locIdentifier) <*> optional (symbol ":" *> parsePolytype) <*> (symbol "=" *> parseTerm <* reserved "end")- <|> parens (mkTuple <$> (parseTerm `sepBy` symbol ","))+ <|> parens (view sTerm . mkTuple <$> (parseTerm `sepBy` symbol ",")) ) -- Potential syntax for explicitly requesting memoized delay. -- Perhaps we will not need this in the end; see the discussion at@@ -281,20 +291,27 @@ <|> parseLoc (SDelay SimpleDelay <$> braces parseTerm) <|> parseLoc (ask >>= (guard . (== AllowAntiquoting)) >> parseAntiquotation) -mkTuple :: [Syntax] -> Term-mkTuple [] = TUnit-mkTuple [STerm x] = x-mkTuple (x : xs) = SPair x (STerm (mkTuple xs))+mkTuple :: [Syntax] -> Syntax+mkTuple [] = Syntax NoLoc TUnit -- should never happen+mkTuple [x] = x+mkTuple (x : xs) = let r = mkTuple xs in loc x r $ SPair x r+ where+ loc a b = Syntax $ (a ^. sLoc) <> (b ^. sLoc) +unTuple :: Syntax' ty -> [Syntax' ty]+unTuple = \case+ Syntax' _ (SPair s1 s2) _ -> s1 : unTuple s2+ s -> [s]+ -- | Construct an 'SLet', automatically filling in the Boolean field -- indicating whether it is recursive.-sLet :: Var -> Maybe Polytype -> Syntax -> Syntax -> Term-sLet x ty t1 = SLet (x `S.member` setOf fv (sTerm t1)) x ty t1+sLet :: LocVar -> Maybe Polytype -> Syntax -> Syntax -> Term+sLet x ty t1 = SLet (lvVar x `S.member` setOf freeVarsV t1) x ty t1 -- | Construct an 'SDef', automatically filling in the Boolean field -- indicating whether it is recursive.-sDef :: Var -> Maybe Polytype -> Syntax -> Term-sDef x ty t = SDef (x `S.member` setOf fv (sTerm t)) x ty t+sDef :: LocVar -> Maybe Polytype -> Syntax -> Term+sDef x ty t = SDef (lvVar x `S.member` setOf freeVarsV t) x ty t parseAntiquotation :: Parser Term parseAntiquotation =@@ -307,23 +324,23 @@ mkBindChain :: [Stmt] -> Parser Syntax mkBindChain stmts = case last stmts of- Binder x _ -> return $ foldr mkBind (STerm (TApp (TConst Return) (TVar x))) stmts+ Binder x _ -> return $ foldr mkBind (STerm (TApp (TConst Return) (TVar (lvVar x)))) stmts BareTerm t -> return $ foldr mkBind t (init stmts) where- mkBind (BareTerm t1) t2 = loc t1 t2 $ SBind Nothing t1 t2- mkBind (Binder x t1) t2 = loc t1 t2 $ SBind (Just x) t1 t2- loc a b = Syntax $ sLoc a <> sLoc b+ mkBind (BareTerm t1) t2 = loc Nothing t1 t2 $ SBind Nothing t1 t2+ mkBind (Binder x t1) t2 = loc (Just x) t1 t2 $ SBind (Just x) t1 t2+ loc mx a b = Syntax $ maybe NoLoc lvSrcLoc mx <> (a ^. sLoc) <> (b ^. sLoc) data Stmt = BareTerm Syntax- | Binder Text Syntax+ | Binder LocVar Syntax deriving (Show) parseStmt :: Parser Stmt parseStmt =- mkStmt <$> optional (try (identifier <* symbol "<-")) <*> parseExpr+ mkStmt <$> optional (try (locIdentifier <* symbol "<-")) <*> parseExpr -mkStmt :: Maybe Text -> Syntax -> Stmt+mkStmt :: Maybe LocVar -> Syntax -> Stmt mkStmt Nothing = BareTerm mkStmt (Just x) = Binder x @@ -339,7 +356,7 @@ [] -> term defs -> foldr1 mkBind defs where- mkBind t1 t2 = Syntax (sLoc t1 <> sLoc t2) $ SBind Nothing t1 t2+ mkBind t1 t2 = Syntax ((t1 ^. sLoc) <> (t2 ^. sLoc)) $ SBind Nothing t1 t2 nestedDefs term' acc = case term' of def@(Syntax _ SDef {}) -> def : acc (Syntax _ (SApp nestedTerm def@(Syntax _ SDef {}))) -> nestedDefs nestedTerm (def : acc)@@ -362,7 +379,7 @@ exprLoc2 :: Parser (Syntax -> Syntax -> Term) -> Parser (Syntax -> Syntax -> Syntax) exprLoc2 p = do (l, f) <- parseLocG p- pure $ \s1 s2 -> Syntax (l <> sLoc s1 <> sLoc s2) $ f s1 s2+ pure $ \s1 s2 -> Syntax (l <> (s1 ^. sLoc) <> (s2 ^. sLoc)) $ f s1 s2 -- | Precedences and parsers of binary operators. --@@ -405,7 +422,7 @@ exprLoc1 :: Parser (Syntax -> Term) -> Parser (Syntax -> Syntax) exprLoc1 p = do (l, f) <- parseLocG p- pure $ \s -> Syntax (l <> sLoc s) $ f s+ pure $ \s -> Syntax (l <> s ^. sLoc) $ f s operatorString :: Text -> Parser Text operatorString n = (lexeme . try) (string n <* notFollowedBy operatorSymbol)@@ -514,7 +531,7 @@ line = unPos $ sourceLine $ pstateSourcePos ps col = unPos $ sourceColumn $ pstateSourcePos ps --- | A utility for converting a Location into a range+-- | A utility for converting a SrcLoc into a range getLocRange :: Text -> (Int, Int) -> ((Int, Int), (Int, Int)) getLocRange code (locStart, locEnd) = (start, end) where
src/Swarm/Language/Pipeline.hs view
@@ -21,6 +21,7 @@ showTypeErrorPos, ) where +import Control.Lens ((^.)) import Data.Bifunctor (first) import Data.Data (Data) import Data.Text (Text)@@ -28,6 +29,7 @@ import GHC.Generics (Generic) import Swarm.Language.Context import Swarm.Language.Elaborate+import Swarm.Language.Module import Swarm.Language.Parse import Swarm.Language.Pretty import Swarm.Language.Requirement@@ -40,10 +42,8 @@ -- pipeline. Put a 'Term' in, and get one of these out. data ProcessedTerm = ProcessedTerm- Term- -- ^ The elaborated term TModule- -- ^ The type of the term (and of any embedded definitions)+ -- ^ The elaborated + type-annotated term, plus types of any embedded definitions Requirements -- ^ Requirements of the term ReqCtx@@ -60,7 +60,7 @@ Right (Just pt) -> return pt instance ToJSON ProcessedTerm where- toJSON (ProcessedTerm t _ _ _) = String $ prettyText t+ toJSON (ProcessedTerm t _ _) = String $ prettyText (moduleAST t) -- | Given a 'Text' value representing a Swarm program, --@@ -87,8 +87,8 @@ prettyTypeErr :: Text -> TypeErr -> Text prettyTypeErr code te = teLoc <> prettyText te where- teLoc = case getTypeErrLocation te of- Just (Location s e) -> (from . show . fst . fst $ getLocRange code (s, e)) <> ": "+ teLoc = case getTypeErrSrcLoc te of+ Just (SrcLoc s e) -> (from . show . fst . fst $ getLocRange code (s, e)) <> ": " _anyOtherLoc -> "" showTypeErrorPos :: Text -> TypeErr -> ((Int, Int), (Int, Int), Text)@@ -96,14 +96,17 @@ where minusOne (x, y) = (x - 1, y - 1) - (start, end) = case getTypeErrLocation te of- Just (Location s e) -> getLocRange code (s, e)+ (start, end) = case getTypeErrSrcLoc te of+ Just (SrcLoc s e) -> getLocRange code (s, e) _anyOtherLoc -> ((1, 1), (65535, 65535)) -- unknown loc spans the whole document msg = prettyText te -- | Like 'processTerm'', but use a term that has already been parsed. processParsedTerm' :: TCtx -> ReqCtx -> Syntax -> Either TypeErr ProcessedTerm processParsedTerm' ctx capCtx t = do- ty <- inferTop ctx t- let (caps, capCtx') = requirements capCtx (sTerm t)- return $ ProcessedTerm (elaborate (sTerm t)) ty caps capCtx'+ m <- inferTop ctx t+ let (caps, capCtx') = requirements capCtx (t ^. sTerm)+ return $ ProcessedTerm (elaborateModule m) caps capCtx'++elaborateModule :: TModule -> TModule+elaborateModule (Module ast ctx) = Module (elaborate ast) ctx
src/Swarm/Language/Pipeline/QQ.hs view
@@ -15,6 +15,7 @@ import Swarm.Language.Pipeline import Swarm.Language.Pretty (prettyText) import Swarm.Language.Syntax+import Swarm.Language.Types (Polytype) import Swarm.Util (liftText) import Witch (from) @@ -48,7 +49,7 @@ Left errMsg -> fail $ from $ prettyText errMsg Right ptm -> dataToExpQ ((fmap liftText . cast) `extQ` antiTermExp) ptm -antiTermExp :: Term -> Maybe TH.ExpQ+antiTermExp :: Term' Polytype -> Maybe TH.ExpQ antiTermExp (TAntiText v) = Just $ TH.appE (TH.conE (TH.mkName "TText")) (TH.varE (TH.mkName (from v))) antiTermExp (TAntiInt v) =
src/Swarm/Language/Pretty.hs view
@@ -61,7 +61,7 @@ prettyPrec _ BDir = "dir" prettyPrec _ BText = "text" prettyPrec _ BBool = "bool"- prettyPrec _ BRobot = "robot"+ prettyPrec _ BActor = "actor" instance PrettyPrec IntVar where prettyPrec _ = pretty . mkVarName "u"@@ -111,6 +111,9 @@ instance PrettyPrec Const where prettyPrec p c = pparens (p > fixity (constInfo c)) $ pretty . syntax . constInfo $ c +instance PrettyPrec (Syntax' ty) where+ prettyPrec p = prettyPrec p . eraseS+ instance PrettyPrec Term where prettyPrec _ TUnit = "()" prettyPrec p (TConst c) = prettyPrec p c@@ -120,10 +123,10 @@ prettyPrec _ (TText s) = fromString (show s) prettyPrec _ (TAntiText v) = "$str:" <> pretty v prettyPrec _ (TBool b) = bool "false" "true" b- prettyPrec _ (TRobot r) = "<r" <> pretty r <> ">"+ prettyPrec _ (TRobot r) = "<a" <> pretty r <> ">" prettyPrec _ (TRef r) = "@" <> pretty r- prettyPrec p (TRequireDevice d) = pparens (p > 10) $ "require" <+> ppr (TText d)- prettyPrec p (TRequire n e) = pparens (p > 10) $ "require" <+> pretty n <+> ppr (TText e)+ prettyPrec p (TRequireDevice d) = pparens (p > 10) $ "require" <+> ppr @Term (TText d)+ prettyPrec p (TRequire n e) = pparens (p > 10) $ "require" <+> pretty n <+> ppr @Term (TText e) prettyPrec _ (TVar s) = pretty s prettyPrec _ (TDelay _ t) = braces $ ppr t prettyPrec _ t@TPair {} = prettyTuple t
src/Swarm/Language/Requirement.hs view
@@ -45,15 +45,15 @@ -- | A /requirement/ is something a robot must have when it is -- built. There are three types: -- - A robot can require a certain 'Capability', which should be fulfilled--- by installing an appropriate device.--- - A robot can require a specific /device/, which should be installed.+-- by equipping an appropriate device.+-- - A robot can require a specific /device/, which should be equipped. -- - A robot can require some number of a specific entity in its inventory. data Requirement = -- | Require a specific capability. This must be fulfilled by- -- installing an appropriate device. Requiring the same+ -- equipping an appropriate device. Requiring the same -- capability multiple times is the same as requiring it once. ReqCap Capability- | -- | Require a specific device to be installed. Note that at this+ | -- | Require a specific device to be equipped. Note that at this -- point it is only a name, and has not been resolved to an actual -- 'Entity'. That's because programs have to be type- and -- capability-checked independent of an 'EntityMap'. The name@@ -230,7 +230,8 @@ -- lambda. TLet r x _ t1 t2 -> (if r then insert (ReqCap CRecursion) else id) $- insert (ReqCap CEnv) $ go (Ctx.delete x ctx) t1 <> go (Ctx.delete x ctx) t2+ insert (ReqCap CEnv) $+ go (Ctx.delete x ctx) t1 <> go (Ctx.delete x ctx) t2 -- We also delete the name in a TBind, if any, while recursing on -- the RHS. TBind mx t1 t2 -> go ctx t1 <> go (maybe id Ctx.delete mx ctx) t2
src/Swarm/Language/Syntax.hs view
@@ -2,7 +2,10 @@ {-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE PatternSynonyms #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE UndecidableInstances #-}+{-# LANGUAGE ViewPatterns #-} -- | -- Module : Swarm.Language.Syntax@@ -15,6 +18,8 @@ module Swarm.Language.Syntax ( -- * Directions Direction (..),+ AbsoluteDir (..),+ RelativeDir (..), DirInfo (..), applyTurn, toDirection,@@ -45,8 +50,14 @@ isLong, -- * Syntax- Syntax (..),- Location (..),+ Syntax' (..),+ sLoc,+ sTerm,+ sType,+ Syntax,+ pattern Syntax,+ LocVar (..),+ SrcLoc (..), noLoc, pattern STerm, pattern TPair,@@ -61,24 +72,35 @@ -- * Terms Var, DelayType (..),- Term (..),+ Term' (..),+ Term, mkOp, mkOp',+ unfoldApps, + -- * Erasure+ erase,+ eraseS,+ -- * Term traversal- fvT,- fv,- mapFree1,+ freeVarsS,+ freeVarsT,+ freeVarsV,+ mapFreeS,+ locVarToSyntax', ) where -import Control.Lens (Plated (..), Traversal', (%~))+import Control.Arrow (Arrow ((&&&)))+import Control.Lens (Plated (..), Traversal', makeLenses, (%~), (^.)) import Data.Aeson.Types+import Data.Char qualified as C (toLower) import Data.Data (Data) import Data.Data.Lens (uniplate) import Data.Hashable (Hashable)-import Data.Int (Int64)+import Data.List qualified as L (tail)+import Data.List.NonEmpty (NonEmpty)+import Data.List.NonEmpty qualified as NonEmpty import Data.Map qualified as M-import Data.Maybe (fromMaybe, isJust, mapMaybe) import Data.Set qualified as S import Data.String (IsString (fromString)) import Data.Text hiding (filter, map)@@ -86,103 +108,135 @@ import GHC.Generics (Generic) import Linear import Swarm.Language.Types+import Swarm.Util qualified as Util+import Swarm.Util.Location (Heading) import Witch.From (from) --------------------------------------------------------------- Constants+-- Directions ------------------------------------------------------------ --- | The type of directions. Used /e.g./ to indicate which way a robot--- will turn.-data Direction = DLeft | DRight | DBack | DForward | DNorth | DSouth | DEast | DWest | DDown+-- | An absolute direction is one which is defined with respect to an+-- external frame of reference; robots need a compass in order to+-- use them.+data AbsoluteDir = DNorth | DSouth | DEast | DWest deriving (Eq, Ord, Show, Read, Generic, Data, Hashable, ToJSON, FromJSON, Enum, Bounded) -instance ToJSONKey Direction where- toJSONKey = genericToJSONKey defaultJSONKeyOptions+cardinalDirectionKeyOptions :: JSONKeyOptions+cardinalDirectionKeyOptions =+ defaultJSONKeyOptions+ { keyModifier = map C.toLower . L.tail+ } -instance FromJSONKey Direction where- fromJSONKey = genericFromJSONKey defaultJSONKeyOptions+instance ToJSONKey AbsoluteDir where+ toJSONKey = genericToJSONKey cardinalDirectionKeyOptions +instance FromJSONKey AbsoluteDir where+ fromJSONKey = genericFromJSONKey cardinalDirectionKeyOptions++-- | A relative direction is one which is defined with respect to the+-- robot's frame of reference; no special capability is needed to+-- use them.+data RelativeDir = DLeft | DRight | DBack | DForward | DDown+ deriving (Eq, Ord, Show, Read, Generic, Data, Hashable, ToJSON, FromJSON, Enum, Bounded)++-- | The type of directions. Used /e.g./ to indicate which way a robot+-- will turn.+data Direction = DAbsolute AbsoluteDir | DRelative RelativeDir+ deriving (Eq, Ord, Show, Read, Generic, Data, Hashable, ToJSON, FromJSON)+ data DirInfo = DirInfo { dirSyntax :: Text- , -- absolute direction if it exists- dirAbs :: Maybe (V2 Int64)- , -- the turning for the direction- dirApplyTurn :: V2 Int64 -> V2 Int64+ , dirApplyTurn :: Heading -> Heading+ -- ^ the turning for the direction } allDirs :: [Direction]-allDirs = [minBound .. maxBound]+allDirs = map DAbsolute Util.listEnums <> map DRelative Util.listEnums +toHeading :: AbsoluteDir -> Heading+toHeading = \case+ DNorth -> north+ DSouth -> south+ DEast -> east+ DWest -> west+ -- | Information about all directions dirInfo :: Direction -> DirInfo dirInfo d = case d of- DLeft -> relative (\(V2 x y) -> V2 (-y) x)- DRight -> relative (\(V2 x y) -> V2 y (-x))- DBack -> relative (\(V2 x y) -> V2 (-x) (-y))- DDown -> relative (const down)- DForward -> relative id- DNorth -> cardinal north- DSouth -> cardinal south- DEast -> cardinal east- DWest -> cardinal west+ DRelative e -> case e of+ DLeft -> relative perp+ DRight -> relative (fmap negate . perp)+ DBack -> relative (fmap negate)+ DDown -> relative (const down)+ DForward -> relative id+ DAbsolute e -> cardinal $ toHeading e where -- name is generate from Direction data constuctor -- e.g. DLeft becomes "left"- directionSyntax = toLower . T.tail . from . show $ d- cardinal v2 = DirInfo directionSyntax (Just v2) (const v2)- relative = DirInfo directionSyntax Nothing+ directionSyntax = toLower . T.tail . from $ case d of+ DAbsolute x -> show x+ DRelative x -> show x + cardinal = DirInfo directionSyntax . const+ relative = DirInfo directionSyntax+ -- | Check if the direction is absolute (e.g. 'north' or 'south'). isCardinal :: Direction -> Bool-isCardinal = isJust . dirAbs . dirInfo+isCardinal = \case+ DAbsolute _ -> True+ _ -> False -- | The cardinal direction north = @V2 0 1@.-north :: V2 Int64+north :: Heading north = V2 0 1 -- | The cardinal direction south = @V2 0 (-1)@.-south :: V2 Int64+south :: Heading south = V2 0 (-1) -- | The cardinal direction east = @V2 1 0@.-east :: V2 Int64+east :: Heading east = V2 1 0 -- | The cardinal direction west = @V2 (-1) 0@.-west :: V2 Int64+west :: Heading west = V2 (-1) 0 -- | The direction for viewing the current cell = @V2 0 0@.-down :: V2 Int64-down = V2 0 0+down :: Heading+down = zero -- | The 'applyTurn' function gives the meaning of each 'Direction' by--- turning relative to the given vector or by turning to an absolute--- direction vector.-applyTurn :: Direction -> V2 Int64 -> V2 Int64+-- turning relative to the given heading or by turning to an absolute+-- heading+applyTurn :: Direction -> Heading -> Heading applyTurn = dirApplyTurn . dirInfo --- | Mapping from heading to their corresponding cardinal directions--- only directions with a 'dirAbs` value are mapped-cardinalDirs :: M.Map (V2 Int64) Direction+-- | Mapping from heading to their corresponding cardinal directions.+-- Only absolute directions are mapped.+cardinalDirs :: M.Map Heading Direction cardinalDirs =- M.fromList- . mapMaybe (\d -> (,d) <$> (dirAbs . dirInfo $ d))- $ allDirs+ M.fromList $ map (toHeading &&& DAbsolute) Util.listEnums --- | Possibly convert a vector into a 'Direction'---that is, if the+-- | Possibly convert a heading into a 'Direction'---that is, if the -- vector happens to be a unit vector in one of the cardinal -- directions.-toDirection :: V2 Int64 -> Maybe Direction+toDirection :: Heading -> Maybe Direction toDirection v = M.lookup v cardinalDirs --- | Convert a 'Direction' into a corresponding vector. Note that+-- | Convert a 'Direction' into a corresponding heading. Note that -- this only does something reasonable for 'DNorth', 'DSouth', 'DEast', -- and 'DWest'---other 'Direction's return the zero vector.-fromDirection :: Direction -> V2 Int64-fromDirection = fromMaybe (V2 0 0) . dirAbs . dirInfo+fromDirection :: Direction -> Heading+fromDirection = \case+ DAbsolute x -> toHeading x+ _ -> zero +------------------------------------------------------------+-- Constants+------------------------------------------------------------+ -- | Constants, representing various built-in functions and commands. -- -- IF YOU ADD A NEW CONSTANT, be sure to also update:@@ -193,9 +247,10 @@ -- 5. the emacs mode syntax highlighter (@contribs/swarm-mode.el@) -- -- GHC will warn you about incomplete pattern matches for the first--- four, so it's not really possible to forget. Note you do not--- need to update the parser or pretty-printer, since they are--- auto-generated from 'constInfo'.+-- four, and CI will warn you about the last, so in theory it's not+-- really possible to forget. Note you do not need to update the+-- parser or pretty-printer, since they are auto-generated from+-- 'constInfo'. data Const = -- Trivial actions @@ -220,14 +275,16 @@ Place | -- | Give an item to another robot at the current location. Give- | -- | Install a device on a robot.- Install+ | -- | Equip a device on oneself.+ Equip+ | -- | Unequip an equipped device, returning to inventory.+ Unequip | -- | Make an item. Make | -- | Sense whether we have a certain item. Has- | -- | Sense whether we have a certain device installed.- Installed+ | -- | Sense whether we have a certain device equipped.+ Equipped | -- | Sense how many of a certain item we have. Count | -- | Drill through an entity.@@ -258,20 +315,28 @@ Time | -- | Get the current x, y coordinates Whereami+ | -- | Get the current heading.+ Heading | -- | See if we can move forward or not. Blocked | -- | Scan a nearby cell Scan | -- | Upload knowledge to another robot Upload- | -- | See if a specific entity is here. (This may be removed.)+ | -- | See if a specific entity is here. Ishere+ | -- | Check whether the current cell is empty+ Isempty | -- | Get a reference to oneself Self | -- | Get the robot's parent Parent | -- | Get a reference to the base Base+ | -- | Meet a nearby robot+ Meet+ | -- | Meet all nearby robots+ MeetAll | -- | Get the robot's display name Whoami | -- | Set the robot's display name@@ -352,6 +417,10 @@ Chars | -- | Split string into two parts. Split+ | -- | Get the character at an index.+ CharAt+ | -- | Create a singleton text value with the given character code.+ ToChar | -- Function composition with nice operators -- | Application operator - helps to avoid parentheses:@@ -371,16 +440,16 @@ Teleport | -- | Run a command as if you were another robot. As- | -- | Find a robot by name.+ | -- | Find an actor by name. RobotNamed- | -- | Find a robot by number.+ | -- | Find an actor by number. RobotNumbered | -- | Check if an entity is known. Knows deriving (Eq, Ord, Enum, Bounded, Data, Show, Generic, FromJSON, ToJSON) allConst :: [Const]-allConst = [minBound .. maxBound]+allConst = Util.listEnums data ConstInfo = ConstInfo { syntax :: Text@@ -451,7 +520,7 @@ -- | The arity of a constant, /i.e./ how many arguments it expects. -- The runtime system will collect arguments to a constant (see--- 'Swarm.Game.Value.VCApp') until it has enough, then dispatch+-- 'Swarm.Language.Value.VCApp') until it has enough, then dispatch -- the constant's behavior. arity :: Const -> Int arity c = case constMeta $ constInfo c of@@ -524,7 +593,7 @@ , "Usually it is automatically inserted where needed, so you do not have to worry about it." ] Selfdestruct ->- command 0 short . doc "Self-destruct the robot." $+ command 0 short . doc "Self-destruct a robot." $ [ "Useful to not clutter the world." , "This destroys the robot's inventory, so consider `salvage` as an alternative." ]@@ -539,11 +608,12 @@ Place -> command 1 short . doc "Place an item at the current location." $ ["The current location has to be empty for this to work."]- Give -> command 2 short "Give an item to another robot nearby."- Install -> command 2 short "Install a device from inventory on a robot."+ Give -> command 2 short "Give an item to another actor nearby."+ Equip -> command 1 short "Equip a device on oneself."+ Unequip -> command 1 short "Unequip an equipped device, returning to inventory." Make -> command 1 long "Make an item using a recipe." Has -> command 1 Intangible "Sense whether the robot has a given item in its inventory."- Installed -> command 1 Intangible "Sense whether the robot has a specific device installed."+ Equipped -> command 1 Intangible "Sense whether the robot has a specific device equipped." Count -> command 1 Intangible "Get the count of a given item in a robot's inventory." Reprogram -> command 2 long . doc "Reprogram another robot with a new command." $@@ -557,29 +627,30 @@ Build -> command 1 long . doc "Construct a new robot." $ [ "You can specify a command for the robot to execute."- , "If the command requires devices they will be installed from your inventory."+ , "If the command requires devices they will be taken from your inventory and "+ <> "equipped on the new robot." ] Salvage -> command 0 long . doc "Deconstruct an old robot." $- ["Salvaging a robot will give you its inventory, installed devices and log."]+ ["Salvaging a robot will give you its inventory, equipped devices and log."] Say -> command 1 short . doc "Emit a message." $- [ "The message will be in the robots log (if it has one) and the global log."+ [ "The message will be in the robot's log (if it has one) and the global log." , "You can view the message that would be picked by `listen` from the global log " <> "in the messages panel, along with your own messages and logs." , "This means that to see messages from other robots you have to be able to listen for them, "- <> "so once you have a listening device installed messages will be added to your log."+ <> "so once you have a listening device equipped messages will be added to your log." , "In creative mode, there is of course no such limitation." ] Listen ->- command 1 long . doc "Listen for a message from other robots." $- [ "It will take the first message said by the closest robot."+ command 1 long . doc "Listen for a message from other actors." $+ [ "It will take the first message said by the closest actor." , "You do not need to actively listen for the message to be logged though, "- <> "that is done automatically once you have a listening device installed."+ <> "that is done automatically once you have a listening device equipped." , "Note that you can see the messages either in your logger device or the message panel." ]- Log -> command 1 Intangible "Log the string in the robot's logger."- View -> command 1 short "View the given robot."+ Log -> command 1 short "Log the string in the robot's logger."+ View -> command 1 short "View the given actor." Appear -> command 1 short . doc "Set how the robot is displayed." $ [ "You can either specify one character or five (for each direction)."@@ -590,17 +661,25 @@ ["Only available in creative mode."] Time -> command 0 Intangible "Get the current time." Whereami -> command 0 Intangible "Get the current x and y coordinates."+ Heading -> command 0 Intangible "Get the current heading." Blocked -> command 0 Intangible "See if the robot can move forward." Scan ->- command 0 Intangible . doc "Scan a nearby location for entities." $- [ "Adds the entity (not robot) to your inventory with count 0 if there is any."+ command 1 Intangible . doc "Scan a nearby location for entities." $+ [ "Adds the entity (not actor) to your inventory with count 0 if there is any." , "If you can use sum types, you can also inspect the result directly." ] Upload -> command 1 short "Upload a robot's known entities and log to another robot." Ishere -> command 1 Intangible "See if a specific entity is in the current location."+ Isempty ->+ command 0 Intangible . doc "Check if the current location is empty." $+ [ "Detects whether or not the current location contains an entity."+ , "Does not detect robots or other actors."+ ] Self -> function 0 "Get a reference to the current robot." Parent -> function 0 "Get a reference to the robot's parent." Base -> function 0 "Get a reference to the base."+ Meet -> command 0 Intangible "Get a reference to a nearby actor, if there is one."+ MeetAll -> command 0 long "Run a command for each nearby actor." Whoami -> command 0 Intangible "Get the robot's display name." Setname -> command 1 short "Set the robot's display name." Random ->@@ -644,6 +723,15 @@ , "`(s1,s2) == split (chars s1) (s1 ++ s2)`" , "So split can be used to undo concatenation if you know the length of the original string." ]+ CharAt ->+ function 2 . doc "Get the character at a given index." $+ [ "Gets the character (as an `int` representing a Unicode codepoint) at a specific index in a `text` value. Valid indices are 0 through `chars t - 1`."+ , "Throws an exception if given an out-of-bounds index."+ ]+ ToChar ->+ function 1 . doc "Create a singleton `text` value from the given character code." $+ [ "That is, `chars (toChar c) == 1` and `charAt 0 (toChar c) == c`."+ ] AppF -> binaryOp "$" 0 R . doc "Apply the function on the left to the value on the right." $ [ "This operator is useful to avoid nesting parentheses."@@ -661,8 +749,8 @@ ] Teleport -> command 2 short "Teleport a robot to the given location." As -> command 2 Intangible "Hypothetically run a command as if you were another robot."- RobotNamed -> command 1 Intangible "Find a robot by name."- RobotNumbered -> command 1 Intangible "Find a robot by number."+ RobotNamed -> command 1 Intangible "Find an actor by name."+ RobotNumbered -> command 1 Intangible "Find an actor by number." Knows -> command 1 Intangible "Check if the robot knows about an entity." where doc b ls = ConstDoc b (T.unlines ls)@@ -699,86 +787,12 @@ , tangibility = Intangible } --- | Make infix operation, discarding any syntax related location-mkOp' :: Const -> Term -> Term -> Term-mkOp' c t1 = TApp (TApp (TConst c) t1)---- | Make infix operation (e.g. @2 + 3@) a curried function--- application (@((+) 2) 3@).-mkOp :: Const -> Syntax -> Syntax -> Syntax-mkOp c s1@(Syntax l1 _) s2@(Syntax l2 _) = Syntax newLoc newTerm- where- -- The new syntax span both terms- newLoc = l1 <> l2- -- We don't assign a source location for the operator since it is- -- usually provided as-is and it is not likely to be useful.- sop = noLoc (TConst c)- newTerm = SApp (Syntax l1 $ SApp sop s1) s2---- | The surface syntax for the language-data Syntax = Syntax {sLoc :: Location, sTerm :: Term}- deriving (Eq, Show, Data, Generic, FromJSON, ToJSON)--data Location = NoLoc | Location Int Int- deriving (Eq, Show, Data, Generic, FromJSON, ToJSON)--instance Semigroup Location where- NoLoc <> l = l- l <> NoLoc = l- Location s1 e1 <> Location s2 e2 = Location (min s1 s2) (max e1 e2)--instance Monoid Location where- mempty = NoLoc--noLoc :: Term -> Syntax-noLoc = Syntax mempty---- | Match a term without its a syntax-pattern STerm :: Term -> Syntax-pattern STerm t <-- Syntax _ t- where- STerm t = Syntax mempty t---- | Match a TPair without syntax-pattern TPair :: Term -> Term -> Term-pattern TPair t1 t2 = SPair (STerm t1) (STerm t2)---- | Match a TLam without syntax-pattern TLam :: Var -> Maybe Type -> Term -> Term-pattern TLam v ty t = SLam v ty (STerm t)---- | Match a TApp without syntax-pattern TApp :: Term -> Term -> Term-pattern TApp t1 t2 = SApp (STerm t1) (STerm t2)--infixl 0 :$:---- | Convenient infix pattern synonym for application.-pattern (:$:) :: Term -> Syntax -> Term-pattern (:$:) t1 s2 = SApp (STerm t1) s2---- | Match a TLet without syntax-pattern TLet :: Bool -> Var -> Maybe Polytype -> Term -> Term -> Term-pattern TLet r v pt t1 t2 = SLet r v pt (STerm t1) (STerm t2)---- | Match a TDef without syntax-pattern TDef :: Bool -> Var -> Maybe Polytype -> Term -> Term-pattern TDef r v pt t = SDef r v pt (STerm t)---- | Match a TBind without syntax-pattern TBind :: Maybe Var -> Term -> Term -> Term-pattern TBind v t1 t2 = SBind v (STerm t1) (STerm t2)---- | Match a TDelay without syntax-pattern TDelay :: DelayType -> Term -> Term-pattern TDelay m t = SDelay m (STerm t)---- | COMPLETE pragma tells GHC using this set of pattern is complete for Term-{-# COMPLETE TUnit, TConst, TDir, TInt, TAntiInt, TText, TAntiText, TBool, TRequireDevice, TRequire, TVar, TPair, TLam, TApp, TLet, TDef, TBind, TDelay #-}+ lowShow :: Show a => a -> Text+ lowShow a = toLower (from (show a)) --------------------------------------------------------------- Terms+-- Basic terms+------------------------------------------------------------ -- | Different runtime behaviors for delayed expressions. data DelayType@@ -797,8 +811,17 @@ MemoizedDelay (Maybe Var) deriving (Eq, Show, Data, Generic, FromJSON, ToJSON) +-- | A variable with associated source location, used for variable+-- binding sites. (Variable occurrences are a bare TVar which gets+-- wrapped in a Syntax node, so we don't need LocVar for those.)+data LocVar = LV {lvSrcLoc :: SrcLoc, lvVar :: Var}+ deriving (Eq, Ord, Show, Data, Generic, FromJSON, ToJSON)++locVarToSyntax' :: LocVar -> ty -> Syntax' ty+locVarToSyntax' (LV s v) = Syntax' s (TVar v)+ -- | Terms of the Swarm language.-data Term+data Term' ty = -- | The unit value. TUnit | -- | A constant.@@ -815,7 +838,7 @@ TAntiText Text | -- | A Boolean literal. TBool Bool- | -- | A robot value. These never show up in surface syntax, but are+ | -- | A robot reference. These never show up in surface syntax, but are -- here so we can factor pretty-printing for Values through -- pretty-printing for Terms. TRobot Int@@ -829,22 +852,22 @@ | -- | A variable. TVar Var | -- | A pair.- SPair Syntax Syntax+ SPair (Syntax' ty) (Syntax' ty) | -- | A lambda expression, with or without a type annotation on the -- binder.- SLam Var (Maybe Type) Syntax+ SLam LocVar (Maybe Type) (Syntax' ty) | -- | Function application.- SApp Syntax Syntax+ SApp (Syntax' ty) (Syntax' ty) | -- | A (recursive) let expression, with or without a type -- annotation on the variable. The @Bool@ indicates whether -- it is known to be recursive.- SLet Bool Var (Maybe Polytype) Syntax Syntax+ SLet Bool LocVar (Maybe Polytype) (Syntax' ty) (Syntax' ty) | -- | A (recursive) definition command, which binds a variable to a -- value in subsequent commands. The @Bool@ indicates whether the -- definition is known to be recursive.- SDef Bool Var (Maybe Polytype) Syntax+ SDef Bool LocVar (Maybe Polytype) (Syntax' ty) | -- | A monadic bind for commands, of the form @c1 ; c2@ or @x <- c1; c2@.- SBind (Maybe Var) Syntax Syntax+ SBind (Maybe LocVar) (Syntax' ty) (Syntax' ty) | -- | Delay evaluation of a term, written @{...}@. Swarm is an -- eager language, but in some cases (e.g. for @if@ statements -- and recursive bindings) we need to delay evaluation. The@@ -852,57 +875,239 @@ -- Note that 'Force' is just a constant, whereas 'SDelay' has to -- be a special syntactic form so its argument can get special -- treatment during evaluation.- SDelay DelayType Syntax- deriving (Eq, Show, Data, Generic, FromJSON, ToJSON)+ SDelay DelayType (Syntax' ty)+ deriving (Eq, Show, Functor, Foldable, Traversable, Data, Generic, FromJSON, ToJSON) -instance Plated Term where+-- The Traversable instance for Term (and for Syntax') is used during+-- typechecking: during intermediate type inference, many of the type+-- annotations placed on AST nodes will have unification variables in+-- them. Once we have finished solving everything we need to do a+-- final traversal over all the types in the AST to substitute away+-- all the unification variables (and generalize, i.e. stick 'forall'+-- on, as appropriate). See the call to 'mapM' in+-- Swarm.Language.Typecheck.runInfer.++type Term = Term' ()++instance Data ty => Plated (Term' ty) where plate = uniplate +------------------------------------------------------------+-- Syntax: annotation on top of Terms with SrcLoc and type+------------------------------------------------------------++-- | The surface syntax for the language, with location and type annotations.+data Syntax' ty = Syntax'+ { _sLoc :: SrcLoc+ , _sTerm :: Term' ty+ , _sType :: ty+ }+ deriving (Eq, Show, Functor, Foldable, Traversable, Data, Generic, FromJSON, ToJSON)++instance Data ty => Plated (Syntax' ty) where+ plate = uniplate++data SrcLoc+ = NoLoc+ | -- | Half-open interval from start (inclusive) to end (exclusive)+ SrcLoc Int Int+ deriving (Eq, Ord, Show, Data, Generic, FromJSON, ToJSON)++instance Semigroup SrcLoc where+ NoLoc <> l = l+ l <> NoLoc = l+ SrcLoc s1 e1 <> SrcLoc s2 e2 = SrcLoc (min s1 s2) (max e1 e2)++instance Monoid SrcLoc where+ mempty = NoLoc++------------------------------------------------------------+-- Pattern synonyms for untyped terms+------------------------------------------------------------++type Syntax = Syntax' ()++pattern Syntax :: SrcLoc -> Term -> Syntax+pattern Syntax l t = Syntax' l t ()++{-# COMPLETE Syntax #-}++makeLenses ''Syntax'++noLoc :: Term -> Syntax+noLoc = Syntax mempty++-- | Match an untyped term without its 'SrcLoc'.+pattern STerm :: Term -> Syntax+pattern STerm t <-+ Syntax _ t+ where+ STerm t = Syntax mempty t++-- | Match a TPair without syntax+pattern TPair :: Term -> Term -> Term+pattern TPair t1 t2 = SPair (STerm t1) (STerm t2)++-- | Match a TLam without syntax+pattern TLam :: Var -> Maybe Type -> Term -> Term+pattern TLam v ty t <- SLam (lvVar -> v) ty (STerm t)+ where+ TLam v ty t = SLam (LV NoLoc v) ty (STerm t)++-- | Match a TApp without syntax+pattern TApp :: Term -> Term -> Term+pattern TApp t1 t2 = SApp (STerm t1) (STerm t2)++infixl 0 :$:++-- | Convenient infix pattern synonym for application.+pattern (:$:) :: Term -> Syntax -> Term+pattern (:$:) t1 s2 = SApp (STerm t1) s2++-- | Match a TLet without syntax+pattern TLet :: Bool -> Var -> Maybe Polytype -> Term -> Term -> Term+pattern TLet r v pt t1 t2 <- SLet r (lvVar -> v) pt (STerm t1) (STerm t2)+ where+ TLet r v pt t1 t2 = SLet r (LV NoLoc v) pt (STerm t1) (STerm t2)++-- | Match a TDef without syntax+pattern TDef :: Bool -> Var -> Maybe Polytype -> Term -> Term+pattern TDef r v pt t <- SDef r (lvVar -> v) pt (STerm t)+ where+ TDef r v pt t = SDef r (LV NoLoc v) pt (STerm t)++-- | Match a TBind without syntax+pattern TBind :: Maybe Var -> Term -> Term -> Term+pattern TBind mv t1 t2 <- SBind (fmap lvVar -> mv) (STerm t1) (STerm t2)+ where+ TBind mv t1 t2 = SBind (LV NoLoc <$> mv) (STerm t1) (STerm t2)++-- | Match a TDelay without syntax+pattern TDelay :: DelayType -> Term -> Term+pattern TDelay m t = SDelay m (STerm t)++-- | COMPLETE pragma tells GHC using this set of pattern is complete for Term+{-# COMPLETE TUnit, TConst, TDir, TInt, TAntiInt, TText, TAntiText, TBool, TRequireDevice, TRequire, TVar, TPair, TLam, TApp, TLet, TDef, TBind, TDelay #-}++-- | Make infix operation (e.g. @2 + 3@) a curried function+-- application (@((+) 2) 3@).+mkOp :: Const -> Syntax -> Syntax -> Syntax+mkOp c s1@(Syntax l1 _) s2@(Syntax l2 _) = Syntax newLoc newTerm+ where+ -- The new syntax span both terms+ newLoc = l1 <> l2+ -- We don't assign a source location for the operator since it is+ -- usually provided as-is and it is not likely to be useful.+ sop = noLoc (TConst c)+ newTerm = SApp (Syntax l1 $ SApp sop s1) s2++-- | Make infix operation, discarding any syntax related location+mkOp' :: Const -> Term -> Term -> Term+mkOp' c t1 = TApp (TApp (TConst c) t1)++-- $setup+-- >>> import Control.Lens ((^.))++-- | Turn function application chain into a list.+--+-- >>> syntaxWrap f = fmap (^. sTerm) . f . Syntax NoLoc+-- >>> syntaxWrap unfoldApps (mkOp' Mul (TInt 1) (TInt 2)) -- 1 * 2+-- TConst Mul :| [TInt 1,TInt 2]+unfoldApps :: Syntax' ty -> NonEmpty (Syntax' ty)+unfoldApps trm = NonEmpty.reverse . flip NonEmpty.unfoldr trm $ \case+ Syntax' _ (SApp s1 s2) _ -> (s2, Just s1)+ s -> (s, Nothing)++--------------------------------------------------+-- Erasure++-- | Erase a 'Syntax' tree annotated with @SrcLoc@ and type+-- information to a bare unannotated 'Term'.+eraseS :: Syntax' ty -> Term+eraseS (Syntax' _ t _) = erase t++-- | Erase a type-annotated term to a bare term.+erase :: Term' ty -> Term+erase TUnit = TUnit+erase (TConst c) = TConst c+erase (TDir d) = TDir d+erase (TInt n) = TInt n+erase (TAntiInt v) = TAntiInt v+erase (TText t) = TText t+erase (TAntiText v) = TAntiText v+erase (TBool b) = TBool b+erase (TRobot r) = TRobot r+erase (TRef r) = TRef r+erase (TRequireDevice d) = TRequireDevice d+erase (TRequire n e) = TRequire n e+erase (TVar s) = TVar s+erase (SDelay x s) = TDelay x (eraseS s)+erase (SPair s1 s2) = TPair (eraseS s1) (eraseS s2)+erase (SLam x mty body) = TLam (lvVar x) mty (eraseS body)+erase (SApp s1 s2) = TApp (eraseS s1) (eraseS s2)+erase (SLet r x mty s1 s2) = TLet r (lvVar x) mty (eraseS s1) (eraseS s2)+erase (SDef r x mty s) = TDef r (lvVar x) mty (eraseS s)+erase (SBind mx s1 s2) = TBind (lvVar <$> mx) (eraseS s1) (eraseS s2)++------------------------------------------------------------+-- Free variable traversals+------------------------------------------------------------+ -- | Traversal over those subterms of a term which represent free--- variables.-fvT :: Traversal' Term Term-fvT f = go S.empty+-- variables. The S suffix indicates that it is a `Traversal' over+-- the `Syntax` nodes (which contain type and source location info)+-- containing free variables inside a larger `Syntax` value. Note+-- that if you want to get the list of all `Syntax` nodes+-- representing free variables, you can do so via @'toListOf'+-- 'freeVarsS'@.+freeVarsS :: forall ty. Traversal' (Syntax' ty) (Syntax' ty)+freeVarsS f = go S.empty where- go bound t = case t of- TUnit -> pure t- TConst {} -> pure t- TDir {} -> pure t- TInt {} -> pure t- TAntiInt {} -> pure t- TText {} -> pure t- TAntiText {} -> pure t- TBool {} -> pure t- TRobot {} -> pure t- TRef {} -> pure t- TRequireDevice {} -> pure t- TRequire {} -> pure t+ -- go :: Applicative f => Set Var -> Syntax' ty -> f (Syntax' ty)+ go bound s@(Syntax' l t ty) = case t of+ TUnit -> pure s+ TConst {} -> pure s+ TDir {} -> pure s+ TInt {} -> pure s+ TAntiInt {} -> pure s+ TText {} -> pure s+ TAntiText {} -> pure s+ TBool {} -> pure s+ TRobot {} -> pure s+ TRef {} -> pure s+ TRequireDevice {} -> pure s+ TRequire {} -> pure s TVar x- | x `S.member` bound -> pure t- | otherwise -> f (TVar x)- SLam x ty (Syntax l1 t1) -> SLam x ty <$> (Syntax l1 <$> go (S.insert x bound) t1)- SApp (Syntax l1 t1) (Syntax l2 t2) ->- SApp <$> (Syntax l1 <$> go bound t1) <*> (Syntax l2 <$> go bound t2)- SLet r x ty (Syntax l1 t1) (Syntax l2 t2) ->- let bound' = S.insert x bound- in SLet r x ty <$> (Syntax l1 <$> go bound' t1) <*> (Syntax l2 <$> go bound' t2)- SPair (Syntax l1 t1) (Syntax l2 t2) ->- SPair <$> (Syntax l1 <$> go bound t1) <*> (Syntax l2 <$> go bound t2)- SDef r x ty (Syntax l1 t1) ->- SDef r x ty <$> (Syntax l1 <$> go (S.insert x bound) t1)- SBind mx (Syntax l1 t1) (Syntax l2 t2) ->- SBind mx <$> (Syntax l1 <$> go bound t1) <*> (Syntax l2 <$> go (maybe id S.insert mx bound) t2)- SDelay m (Syntax l1 t1) ->- SDelay m <$> (Syntax l1 <$> go bound t1)+ | x `S.member` bound -> pure s+ | otherwise -> f s+ SLam x xty s1 -> rewrap $ SLam x xty <$> go (S.insert (lvVar x) bound) s1+ SApp s1 s2 -> rewrap $ SApp <$> go bound s1 <*> go bound s2+ SLet r x xty s1 s2 ->+ let bound' = S.insert (lvVar x) bound+ in rewrap $ SLet r x xty <$> go bound' s1 <*> go bound' s2+ SPair s1 s2 -> rewrap $ SPair <$> go bound s1 <*> go bound s2+ SDef r x xty s1 -> rewrap $ SDef r x xty <$> go (S.insert (lvVar x) bound) s1+ SBind mx s1 s2 -> rewrap $ SBind mx <$> go bound s1 <*> go (maybe id (S.insert . lvVar) mx bound) s2+ SDelay m s1 -> rewrap $ SDelay m <$> go bound s1+ where+ rewrap s' = Syntax' l <$> s' <*> pure ty --- | Traversal over the free variables of a term. Note that if you--- want to get the set of all free variables, you can do so via--- @'Data.Set.Lens.setOf' 'fv'@.-fv :: Traversal' Term Var-fv = fvT . (\f -> \case TVar x -> TVar <$> f x; t -> pure t)+-- | Like 'freeVarsS', but traverse over the 'Term's containing free+-- variables. More direct if you don't need to know the types or+-- source locations of the variables. Note that if you want to get+-- the list of all `Term`s representing free variables, you can do so via+-- @'toListOf' 'freeVarsT'@.+freeVarsT :: forall ty. Traversal' (Syntax' ty) (Term' ty)+freeVarsT = freeVarsS . sTerm --- | Apply a function to all free occurrences of a particular variable.-mapFree1 :: Var -> (Term -> Term) -> Term -> Term-mapFree1 x f = fvT %~ (\t -> if t == TVar x then f t else t)+-- | Traversal over the free variables of a term. Like 'freeVarsS'+-- and 'freeVarsT', but traverse over the variable names+-- themselves. Note that if you want to get the set of all free+-- variable names, you can do so via @'Data.Set.Lens.setOf'+-- 'freeVarsV'@.+freeVarsV :: Traversal' (Syntax' ty) Var+freeVarsV = freeVarsT . (\f -> \case TVar x -> TVar <$> f x; t -> pure t) -lowShow :: Show a => a -> Text-lowShow a = toLower (from (show a))+-- | Apply a function to all free occurrences of a particular variable.+mapFreeS :: Var -> (Syntax' ty -> Syntax' ty) -> Syntax' ty -> Syntax' ty+mapFreeS x f = freeVarsS %~ (\t -> case t ^. sTerm of TVar y | y == x -> f t; _ -> t)
src/Swarm/Language/Typecheck.hs view
@@ -1,5 +1,7 @@+{-# LANGUAGE InstanceSigs #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE ViewPatterns #-} {-# OPTIONS_GHC -fno-warn-orphans #-} -- For 'Ord IntVar' instance@@ -18,7 +20,7 @@ -- * Type errors TypeErr (..), InvalidAtomicReason (..),- getTypeErrLocation,+ getTypeErrSrcLoc, -- * Inference monad Infer,@@ -46,13 +48,16 @@ ) where import Control.Category ((>>>))+import Control.Lens ((^.)) import Control.Monad.Except import Control.Monad.Reader import Control.Unification hiding (applyBindings, (=:=)) import Control.Unification qualified as U import Control.Unification.IntVar+import Data.Data (Data, gmapM) import Data.Foldable (fold) import Data.Functor.Identity+import Data.Generics (mkM) import Data.Map (Map) import Data.Map qualified as M import Data.Maybe@@ -60,6 +65,7 @@ import Data.Set qualified as S import Swarm.Language.Context hiding (lookup) import Swarm.Language.Context qualified as Ctx+import Swarm.Language.Module import Swarm.Language.Parse.QQ (tyQ) import Swarm.Language.Syntax import Swarm.Language.Types@@ -79,7 +85,12 @@ runInfer :: TCtx -> Infer UModule -> Either TypeErr TModule runInfer ctx = (>>= applyBindings)- >>> (>>= \(Module uty uctx) -> Module <$> (fromU <$> generalize uty) <*> pure (fromU uctx))+ >>> ( >>=+ \(Module u uctx) ->+ Module+ <$> mapM (fmap fromU . generalize) u+ <*> pure (fromU uctx)+ ) >>> flip runReaderT (toU ctx) >>> runExceptT >>> evalIntBindingT@@ -89,7 +100,7 @@ -- an 'UnboundVar' error if it is not found, or opening its -- associated 'UPolytype' with fresh unification variables via -- 'instantiate'.-lookup :: Location -> Var -> Infer UType+lookup :: SrcLoc -> Var -> Infer UType lookup loc x = do ctx <- ask maybe (throwError $ UnboundVar loc x) instantiate (Ctx.lookup x ctx)@@ -142,8 +153,8 @@ infix 4 =:= -- | Constrain two types to be equal.-(=:=) :: UType -> UType -> Infer ()-s =:= t = void (lift $ s U.=:= t)+(=:=) :: UType -> UType -> Infer UType+s =:= t = lift $ s U.=:= t -- | @unification-fd@ provides a function 'U.applyBindings' which -- fully substitutes for any bound unification variables (for@@ -163,8 +174,14 @@ instance HasBindings UCtx where applyBindings = mapM applyBindings +instance (HasBindings u, Data u) => HasBindings (Term' u) where+ applyBindings = gmapM (mkM (applyBindings @(Syntax' u)))++instance (HasBindings u, Data u) => HasBindings (Syntax' u) where+ applyBindings (Syntax' l t u) = Syntax' l <$> applyBindings t <*> applyBindings u+ instance HasBindings UModule where- applyBindings (Module uty uctx) = Module <$> applyBindings uty <*> applyBindings uctx+ applyBindings (Module u uctx) = Module <$> applyBindings u <*> applyBindings uctx ------------------------------------------------------------ -- Converting between mono- and polytypes@@ -212,25 +229,25 @@ -- separately be pretty-printed to display them to the user. data TypeErr = -- | An undefined variable was encountered.- UnboundVar Location Var+ UnboundVar SrcLoc Var | -- | A Skolem variable escaped its local context.- EscapedSkolem Location Var+ EscapedSkolem SrcLoc Var | Infinite IntVar UType | -- | The given term was expected to have a certain type, but has a -- different type instead.- Mismatch Location (TypeF UType) (TypeF UType)+ Mismatch SrcLoc (TypeF UType) (TypeF UType) | -- | A definition was encountered not at the top level.- DefNotTopLevel Location Term+ DefNotTopLevel SrcLoc Term | -- | A term was encountered which we cannot infer the type of. -- This should never happen.- CantInfer Location Term+ CantInfer SrcLoc Term | -- | An invalid argument was provided to @atomic@.- InvalidAtomic Location InvalidAtomicReason Term+ InvalidAtomic SrcLoc InvalidAtomicReason Term deriving (Show) -- | Various reasons the body of an @atomic@ might be invalid. data InvalidAtomicReason- = -- | The arugment has too many tangible commands.+ = -- | The argument has too many tangible commands. TooManyTicks Int | -- | The argument uses some way to duplicate code: @def@, @let@, or lambda. AtomicDupingThing@@ -246,8 +263,8 @@ occursFailure = Infinite mismatchFailure = Mismatch NoLoc -getTypeErrLocation :: TypeErr -> Maybe Location-getTypeErrLocation te = case te of+getTypeErrSrcLoc :: TypeErr -> Maybe SrcLoc+getTypeErrSrcLoc te = case te of UnboundVar l _ -> Just l EscapedSkolem l _ -> Just l Infinite _ _ -> Nothing@@ -268,33 +285,33 @@ -- | Infer the signature of a top-level expression which might -- contain definitions. inferModule :: Syntax -> Infer UModule-inferModule s@(Syntax _ t) = (`catchError` addLocToTypeErr s) $ case t of+inferModule s@(Syntax l t) = (`catchError` addLocToTypeErr s) $ case t of -- For definitions with no type signature, make up a fresh type -- variable for the body, infer the body under an extended context, -- and unify the two. Then generalize the type and return an -- appropriate context.- SDef _ x Nothing t1 -> do+ SDef r x Nothing t1 -> do xTy <- fresh- ty <- withBinding x (Forall [] xTy) $ infer t1- xTy =:= ty- pty <- generalize ty- return $ Module (UTyCmd UTyUnit) (singleton x pty)+ t1' <- withBinding (lvVar x) (Forall [] xTy) $ infer t1+ _ <- xTy =:= t1' ^. sType+ pty <- generalize (t1' ^. sType)+ return $ Module (Syntax' l (SDef r x Nothing t1') (UTyCmd UTyUnit)) (singleton (lvVar x) pty) -- If a (poly)type signature has been provided, skolemize it and -- check the definition.- SDef _ x (Just pty) t1 -> do+ SDef r x (Just pty) t1 -> do let upty = toU pty uty <- skolemize upty- withBinding x upty $ check t1 uty- return $ Module (UTyCmd UTyUnit) (singleton x upty)+ t1' <- withBinding (lvVar x) upty $ check t1 uty+ return $ Module (Syntax' l (SDef r x (Just pty) t1') (UTyCmd UTyUnit)) (singleton (lvVar x) upty) -- To handle a 'TBind', infer the types of both sides, combining the -- returned modules appropriately. Have to be careful to use the -- correct context when checking the right-hand side in particular. SBind mx c1 c2 -> do -- First, infer the left side.- Module cmda ctx1 <- inferModule c1- a <- decomposeCmdTy cmda+ Module c1' ctx1 <- inferModule c1+ a <- decomposeCmdTy (c1' ^. sType) -- Now infer the right side under an extended context: things in -- scope on the right-hand side include both any definitions@@ -304,47 +321,55 @@ -- case the bound x should shadow the defined one; hence, we apply -- that binding /after/ (i.e. /within/) the application of @ctx1@. withBindings ctx1 $- maybe id (`withBinding` Forall [] a) mx $ do- Module cmdb ctx2 <- inferModule c2+ maybe id ((`withBinding` Forall [] a) . lvVar) mx $ do+ Module c2' ctx2 <- inferModule c2 - -- We don't actually need the result type since we're just going- -- to return cmdb, but it's important to ensure it's a command- -- type anyway. Otherwise something like 'move; 3' would be- -- accepted with type int.- _ <- decomposeCmdTy cmdb+ -- We don't actually need the result type since we're just+ -- going to return the entire type, but it's important to+ -- ensure it's a command type anyway. Otherwise something+ -- like 'move; 3' would be accepted with type int.+ _ <- decomposeCmdTy (c2' ^. sType) -- Ctx.union is right-biased, so ctx1 `union` ctx2 means later -- definitions will shadow previous ones. Include the binder -- (if any) as well, since binders are made available at the top -- level, just like definitions. e.g. if the user writes `r <- build {move}`, -- then they will be able to refer to r again later.- let ctxX = maybe Ctx.empty (`Ctx.singleton` Forall [] a) mx- return $ Module cmdb (ctx1 `Ctx.union` ctxX `Ctx.union` ctx2)+ let ctxX = maybe Ctx.empty ((`Ctx.singleton` Forall [] a) . lvVar) mx+ return $+ Module+ (Syntax' l (SBind mx c1' c2') (c2' ^. sType))+ (ctx1 `Ctx.union` ctxX `Ctx.union` ctx2) -- In all other cases, there can no longer be any definitions in the -- term, so delegate to 'infer'. _anyOtherTerm -> trivMod <$> infer s --- | Infer the type of a term which does not contain definitions.-infer :: Syntax -> Infer UType+-- | Infer the type of a term which does not contain definitions,+-- returning a type-annotated term.+infer :: Syntax -> Infer (Syntax' UType) infer s@(Syntax l t) = (`catchError` addLocToTypeErr s) $ case t of- TUnit -> return UTyUnit- TConst c -> instantiate . toU $ inferConst c- TDir _ -> return UTyDir- TInt _ -> return UTyInt- TAntiInt _ -> return UTyInt- TText _ -> return UTyText- TAntiText _ -> return UTyText- TBool _ -> return UTyBool- TRobot _ -> return UTyRobot+ TUnit -> return $ Syntax' l TUnit UTyUnit+ TConst c -> Syntax' l (TConst c) <$> (instantiate . toU $ inferConst c)+ TDir d -> return $ Syntax' l (TDir d) UTyDir+ TInt n -> return $ Syntax' l (TInt n) UTyInt+ TAntiInt x -> return $ Syntax' l (TAntiInt x) UTyInt+ TText x -> return $ Syntax' l (TText x) UTyText+ TAntiText x -> return $ Syntax' l (TAntiText x) UTyText+ TBool b -> return $ Syntax' l (TBool b) UTyBool+ TRobot r -> return $ Syntax' l (TRobot r) UTyActor -- We should never encounter a TRef since they do not show up in -- surface syntax, only as values while evaluating (*after* -- typechecking). TRef _ -> throwError $ CantInfer l t- TRequireDevice _ -> return $ UTyCmd UTyUnit- TRequire _ _ -> return $ UTyCmd UTyUnit+ TRequireDevice d -> return $ Syntax' l (TRequireDevice d) (UTyCmd UTyUnit)+ TRequire n d -> return $ Syntax' l (TRequire n d) (UTyCmd UTyUnit) -- To infer the type of a pair, just infer both components.- SPair t1 t2 -> UTyProd <$> infer t1 <*> infer t2+ SPair t1 t2 -> do+ t1' <- infer t1+ t2' <- infer t2+ return $ Syntax' l (SPair t1' t2') (UTyProd (t1' ^. sType) (t2' ^. sType))+ -- if t : ty, then {t} : {ty}. -- Note that in theory, if the @Maybe Var@ component of the @SDelay@ -- is @Just@, we should typecheck the body under a context extended@@ -354,74 +379,83 @@ -- @SDelay@ nodes are never generated from the surface syntax, only -- dynamically at runtime when evaluating recursive let or def expressions, -- so we don't have to worry about typechecking them here.- SDelay _ dt -> UTyDelay <$> infer dt+ SDelay d t1 -> do+ t1' <- infer t1+ return $ Syntax' l (SDelay d t1') (UTyDelay (t1' ^. sType))+ -- We need a special case for checking the argument to 'atomic'. -- 'atomic t' has the same type as 't', which must have a type of -- the form 'cmd a'. 't' must also be syntactically free of -- variables.+ TConst Atomic :$: at -> do argTy <- fresh- check at (UTyCmd argTy)+ at' <- check at (UTyCmd argTy)+ atomic' <- infer (Syntax l (TConst Atomic)) -- It's important that we typecheck the subterm @at@ *before* we -- check that it is a valid argument to @atomic@: this way we can -- ensure that we have already inferred the types of any variables -- referenced. validAtomic at- return $ UTyCmd argTy+ return $ Syntax' l (SApp atomic' at') (at' ^. sType) -- Just look up variables in the context.- TVar x -> lookup l x+ TVar x -> Syntax' l (TVar x) <$> lookup l x -- To infer the type of a lambda if the type of the argument is -- provided, just infer the body under an extended context and return -- the appropriate function type. SLam x (Just argTy) lt -> do let uargTy = toU argTy- resTy <- withBinding x (Forall [] uargTy) $ infer lt- return $ UTyFun uargTy resTy+ lt' <- withBinding (lvVar x) (Forall [] uargTy) $ infer lt+ return $ Syntax' l (SLam x (Just argTy) lt') (UTyFun uargTy (lt' ^. sType)) -- If the type of the argument is not provided, create a fresh -- unification variable for it and proceed. SLam x Nothing lt -> do argTy <- fresh- resTy <- withBinding x (Forall [] argTy) $ infer lt- return $ UTyFun argTy resTy+ lt' <- withBinding (lvVar x) (Forall [] argTy) $ infer lt+ return $ Syntax' l (SLam x Nothing lt') (UTyFun argTy (lt' ^. sType)) -- To infer the type of an application: SApp f x -> do -- Infer the type of the left-hand side and make sure it has a function type.- fTy <- infer f- (ty1, ty2) <- decomposeFunTy fTy+ f' <- infer f+ (ty1, ty2) <- decomposeFunTy (f' ^. sType) -- Then check that the argument has the right type.- check x ty1 `catchError` addLocToTypeErr x- return ty2+ x' <- check x ty1 `catchError` addLocToTypeErr x+ return $ Syntax' l (SApp f' x') ty2 -- We can infer the type of a let whether a type has been provided for -- the variable or not.- SLet _ x Nothing t1 t2 -> do+ SLet r x Nothing t1 t2 -> do xTy <- fresh- uty <- withBinding x (Forall [] xTy) $ infer t1- xTy =:= uty+ t1' <- withBinding (lvVar x) (Forall [] xTy) $ infer t1+ let uty = t1' ^. sType+ _ <- xTy =:= uty upty <- generalize uty- withBinding x upty $ infer t2- SLet _ x (Just pty) t1 t2 -> do+ t2' <- withBinding (lvVar x) upty $ infer t2+ return $ Syntax' l (SLet r x Nothing t1' t2') (t2' ^. sType)+ SLet r x (Just pty) t1 t2 -> do let upty = toU pty -- If an explicit polytype has been provided, skolemize it and check -- definition and body under an extended context. uty <- skolemize upty- resTy <- withBinding x upty $ do- check t1 uty `catchError` addLocToTypeErr t1- infer t2+ (t1', t2') <- withBinding (lvVar x) upty $ do+ (,)+ <$> check t1 uty+ `catchError` addLocToTypeErr t1+ <*> infer t2 -- Make sure no skolem variables have escaped. ask >>= mapM_ noSkolems- return resTy+ return $ Syntax' l (SLet r x (Just pty) t1' t2') (t2' ^. sType) SDef {} -> throwError $ DefNotTopLevel l t SBind mx c1 c2 -> do- ty1 <- infer c1- a <- decomposeCmdTy ty1- ty2 <- maybe id (`withBinding` Forall [] a) mx $ infer c2- _ <- decomposeCmdTy ty2- return ty2+ c1' <- infer c1+ a <- decomposeCmdTy (c1' ^. sType)+ c2' <- maybe id ((`withBinding` Forall [] a) . lvVar) mx $ infer c2+ _ <- decomposeCmdTy (c2' ^. sType)+ return $ Syntax' l (SBind mx c1' c2') (c2' ^. sType) where noSkolems :: UPolytype -> Infer () noSkolems (Forall xs upty) = do@@ -433,11 +467,12 @@ upty' ftyvs = tyvs `S.difference` S.fromList xs unless (S.null ftyvs) $- throwError $ EscapedSkolem l (head (S.toList ftyvs))+ throwError $+ EscapedSkolem l (head (S.toList ftyvs)) -addLocToTypeErr :: Syntax -> TypeErr -> Infer a+addLocToTypeErr :: Syntax' ty -> TypeErr -> Infer a addLocToTypeErr s te = case te of- Mismatch NoLoc a b -> throwError $ Mismatch (sLoc s) a b+ Mismatch NoLoc a b -> throwError $ Mismatch (s ^. sLoc) a b _ -> throwError te -- | Decompose a type that is supposed to be a command type.@@ -445,7 +480,7 @@ decomposeCmdTy (UTyCmd a) = return a decomposeCmdTy ty = do a <- fresh- ty =:= UTyCmd a+ _ <- ty =:= UTyCmd a return a -- | Decompose a type that is supposed to be a function type.@@ -454,7 +489,7 @@ decomposeFunTy ty = do ty1 <- fresh ty2 <- fresh- ty =:= UTyFun ty1 ty2+ _ <- ty =:= UTyFun ty1 ty2 return (ty1, ty2) -- | Infer the type of a constant.@@ -468,31 +503,36 @@ Grab -> [tyQ| cmd text |] Harvest -> [tyQ| cmd text |] Place -> [tyQ| text -> cmd unit |]- Give -> [tyQ| robot -> text -> cmd unit |]- Install -> [tyQ| robot -> text -> cmd unit |]+ Give -> [tyQ| actor -> text -> cmd unit |]+ Equip -> [tyQ| text -> cmd unit |]+ Unequip -> [tyQ| text -> cmd unit |] Make -> [tyQ| text -> cmd unit |] Has -> [tyQ| text -> cmd bool |]- Installed -> [tyQ| text -> cmd bool |]+ Equipped -> [tyQ| text -> cmd bool |] Count -> [tyQ| text -> cmd int |]- Reprogram -> [tyQ| robot -> {cmd a} -> cmd unit |]- Build -> [tyQ| {cmd a} -> cmd robot |]+ Reprogram -> [tyQ| actor -> {cmd a} -> cmd unit |]+ Build -> [tyQ| {cmd a} -> cmd actor |] Drill -> [tyQ| dir -> cmd unit |] Salvage -> [tyQ| cmd unit |] Say -> [tyQ| text -> cmd unit |] Listen -> [tyQ| cmd text |] Log -> [tyQ| text -> cmd unit |]- View -> [tyQ| robot -> cmd unit |]+ View -> [tyQ| actor -> cmd unit |] Appear -> [tyQ| text -> cmd unit |] Create -> [tyQ| text -> cmd unit |] Time -> [tyQ| cmd int |] Whereami -> [tyQ| cmd (int * int) |]+ Heading -> [tyQ| cmd dir |] Blocked -> [tyQ| cmd bool |] Scan -> [tyQ| dir -> cmd (unit + text) |]- Upload -> [tyQ| robot -> cmd unit |]+ Upload -> [tyQ| actor -> cmd unit |] Ishere -> [tyQ| text -> cmd bool |]- Self -> [tyQ| robot |]- Parent -> [tyQ| robot |]- Base -> [tyQ| robot |]+ Isempty -> [tyQ| cmd bool |]+ Self -> [tyQ| actor |]+ Parent -> [tyQ| actor |]+ Base -> [tyQ| actor |]+ Meet -> [tyQ| cmd (unit + actor) |]+ MeetAll -> [tyQ| (b -> actor -> cmd b) -> b -> cmd b |] Whoami -> [tyQ| cmd text |] Setname -> [tyQ| text -> cmd unit |] Random -> [tyQ| int -> cmd int |]@@ -527,24 +567,27 @@ Concat -> [tyQ| text -> text -> text |] Chars -> [tyQ| text -> int |] Split -> [tyQ| int -> text -> (text * text) |]+ CharAt -> [tyQ| int -> text -> int |]+ ToChar -> [tyQ| int -> text |] AppF -> [tyQ| (a -> b) -> a -> b |] Swap -> [tyQ| text -> cmd text |] Atomic -> [tyQ| cmd a -> cmd a |]- Teleport -> [tyQ| robot -> (int * int) -> cmd unit |]- As -> [tyQ| robot -> {cmd a} -> cmd a |]- RobotNamed -> [tyQ| text -> cmd robot |]- RobotNumbered -> [tyQ| int -> cmd robot |]+ Teleport -> [tyQ| actor -> (int * int) -> cmd unit |]+ As -> [tyQ| actor -> {cmd a} -> cmd a |]+ RobotNamed -> [tyQ| text -> cmd actor |]+ RobotNumbered -> [tyQ| int -> cmd actor |] Knows -> [tyQ| text -> cmd bool |] where cmpBinT = [tyQ| a -> a -> bool |] arithBinT = [tyQ| int -> int -> int |] --- | @check t ty@ checks that @t@ has type @ty@.-check :: Syntax -> UType -> Infer ()+-- | @check t ty@ checks that @t@ has type @ty@, returning a+-- type-annotated AST if so.+check :: Syntax -> UType -> Infer (Syntax' UType) check t ty = do- ty' <- infer t- _ <- ty =:= ty'- return ()+ Syntax' l t' ty' <- infer t+ theTy <- ty =:= ty'+ return $ Syntax' l t' theTy -- | Ensure a term is a valid argument to @atomic@. Valid arguments -- may not contain @def@, @let@, or lambda. Any variables which are@@ -614,37 +657,37 @@ SDelay _ s1 -> analyzeAtomic locals s1 -- Bind is similarly simple except that we have to keep track of a local variable -- bound in the RHS.- SBind mx s1 s2 -> (+) <$> analyzeAtomic locals s1 <*> analyzeAtomic (maybe id S.insert mx locals) s2+ SBind mx s1 s2 -> (+) <$> analyzeAtomic locals s1 <*> analyzeAtomic (maybe id (S.insert . lvVar) mx locals) s2 -- Variables are allowed if bound locally, or if they have a simple type. TVar x | x `S.member` locals -> return 0 | otherwise -> do- mxTy <- asks $ Ctx.lookup x- case mxTy of- -- If the variable is undefined, return 0 to indicate the- -- atomic block is valid, because we'd rather have the error- -- caught by the real name+type checking.- Nothing -> return 0- Just xTy -> do- -- Use applyBindings to make sure that we apply as much- -- information as unification has learned at this point. In- -- theory, continuing to typecheck other terms elsewhere in- -- the program could give us further information about xTy,- -- so we might have incomplete information at this point.- -- However, since variables referenced in an atomic block- -- must necessarily have simple types, it's unlikely this- -- will really make a difference. The alternative, more- -- "correct" way to do this would be to simply emit some- -- constraints at this point saying that xTy must be a- -- simple type, and check later that the constraint holds,- -- after performing complete type inference. However, since- -- the current approach is much simpler, we'll stick with- -- this until such time as we have concrete examples showing- -- that the more correct, complex way is necessary.- xTy' <- applyBindings xTy- if isSimpleUPolytype xTy'- then return 0- else throwError (InvalidAtomic l (NonSimpleVarType x xTy') t)+ mxTy <- asks $ Ctx.lookup x+ case mxTy of+ -- If the variable is undefined, return 0 to indicate the+ -- atomic block is valid, because we'd rather have the error+ -- caught by the real name+type checking.+ Nothing -> return 0+ Just xTy -> do+ -- Use applyBindings to make sure that we apply as much+ -- information as unification has learned at this point. In+ -- theory, continuing to typecheck other terms elsewhere in+ -- the program could give us further information about xTy,+ -- so we might have incomplete information at this point.+ -- However, since variables referenced in an atomic block+ -- must necessarily have simple types, it's unlikely this+ -- will really make a difference. The alternative, more+ -- "correct" way to do this would be to simply emit some+ -- constraints at this point saying that xTy must be a+ -- simple type, and check later that the constraint holds,+ -- after performing complete type inference. However, since+ -- the current approach is much simpler, we'll stick with+ -- this until such time as we have concrete examples showing+ -- that the more correct, complex way is necessary.+ xTy' <- applyBindings xTy+ if isSimpleUPolytype xTy'+ then return 0+ else throwError (InvalidAtomic l (NonSimpleVarType x xTy') t) -- No lambda, `let` or `def` allowed! SLam {} -> throwError (InvalidAtomic l AtomicDupingThing t) SLet {} -> throwError (InvalidAtomic l AtomicDupingThing t)
src/Swarm/Language/Types.hs view
@@ -29,7 +29,7 @@ pattern TyText, pattern TyDir, pattern TyBool,- pattern TyRobot,+ pattern TyActor, pattern (:+:), pattern (:*:), pattern (:->:),@@ -46,7 +46,7 @@ pattern UTyText, pattern UTyDir, pattern UTyBool,- pattern UTyRobot,+ pattern UTyActor, pattern UTySum, pattern UTyProd, pattern UTyFun,@@ -67,12 +67,6 @@ TCtx, UCtx, - -- * Modules- Module (..),- TModule,- UModule,- trivMod,- -- * The 'WithU' class WithU (..), ) where@@ -111,8 +105,11 @@ BDir | -- | Booleans. BBool- | -- | Robots.- BRobot+ | -- | "Actors", i.e. anything that can do stuff. Internally, these+ -- are all just "robots", but we give them a more generic+ -- in-game name because they could represent other things like+ -- aliens, animals, seeds, ...+ BActor deriving (Eq, Ord, Show, Data, Generic, FromJSON, ToJSON) -- | A "structure functor" encoding the shape of type expressions.@@ -214,34 +211,6 @@ type UPolytype = Poly UType --------------------------------------------------------------- Modules----------------------------------------------------------------- | A module generally represents the result of performing type--- inference on a top-level expression, which in particular can--- contain definitions ('Swarm.Language.Syntax.TDef'). A module--- contains the overall type of the expression, as well as the--- context giving the types of any defined variables.-data Module s t = Module {moduleTy :: s, moduleCtx :: Ctx t}- deriving (Show, Eq, Functor, Data, Generic, FromJSON, ToJSON)---- | A 'TModule' is the final result of the type inference process on--- an expression: we get a polytype for the expression, and a--- context of polytypes for the defined variables.-type TModule = Module Polytype Polytype---- | A 'UModule' represents the type of an expression at some--- intermediate stage during the type inference process. We get a--- 'UType' (/not/ a 'UPolytype') for the expression, which may--- contain some free unification or type variables, as well as a--- context of 'UPolytype's for any defined variables.-type UModule = Module UType UPolytype---- | The trivial module for a given @s@, with the empty context.-trivMod :: s -> Module s t-trivMod t = Module t empty-------------------------------------------------------------- -- WithU ------------------------------------------------------------ @@ -312,8 +281,8 @@ pattern TyBool :: Type pattern TyBool = Fix (TyBaseF BBool) -pattern TyRobot :: Type-pattern TyRobot = Fix (TyBaseF BRobot)+pattern TyActor :: Type+pattern TyActor = Fix (TyBaseF BActor) infixr 5 :+: @@ -360,8 +329,8 @@ pattern UTyBool :: UType pattern UTyBool = UTerm (TyBaseF BBool) -pattern UTyRobot :: UType-pattern UTyRobot = UTerm (TyBaseF BRobot)+pattern UTyActor :: UType+pattern UTyActor = UTerm (TyBaseF BActor) pattern UTySum :: UType -> UType -> UType pattern UTySum ty1 ty2 = UTerm (TySumF ty1 ty2)
+ src/Swarm/Language/Value.hs view
@@ -0,0 +1,122 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE GADTs #-}++-- |+-- Module : Swarm.Language.Value+-- Copyright : Brent Yorgey+-- Maintainer : byorgey@gmail.com+--+-- SPDX-License-Identifier: BSD-3-Clause+--+-- Values and environments used for interpreting the Swarm language.+module Swarm.Language.Value (+ -- * Values+ Value (..),+ stripVResult,+ prettyValue,+ valueToTerm,++ -- * Environments+ Env,+) where++import Data.Aeson (FromJSON, ToJSON)+import Data.Bool (bool)+import Data.List (foldl')+import Data.Map qualified as M+import Data.Set qualified as S+import Data.Set.Lens (setOf)+import Data.Text (Text)+import GHC.Generics (Generic)+import Swarm.Language.Context+import Swarm.Language.Pretty (prettyText)+import Swarm.Language.Syntax+import Prelude++-- | A /value/ is a term that cannot (or does not) take any more+-- evaluation steps on its own.+data Value where+ -- | The unit value.+ VUnit :: Value+ -- | An integer.+ VInt :: Integer -> Value+ -- | Literal text.+ VText :: Text -> Value+ -- | A direction.+ VDir :: Direction -> Value+ -- | A boolean.+ VBool :: Bool -> Value+ -- | A reference to a robot.+ VRobot :: Int -> Value+ -- | An injection into a sum type. False = left, True = right.+ VInj :: Bool -> Value -> Value+ -- | A pair.+ VPair :: Value -> Value -> Value+ -- | A /closure/, representing a lambda term along with an+ -- environment containing bindings for any free variables in the+ -- body of the lambda.+ VClo :: Var -> Term -> Env -> Value+ -- | An application of a constant to some value arguments,+ -- potentially waiting for more arguments. If a constant+ -- application is fully saturated (as defined by its 'arity'),+ -- whether it is a value or not depends on whether or not it+ -- represents a command (as defined by 'isCmd'). If a command+ -- (e.g. 'Build'), it is a value, and awaits an 'Swarm.Game.CESK.FExec' frame+ -- which will cause it to execute. Otherwise (e.g. 'If'), it is+ -- not a value, and will immediately reduce.+ VCApp :: Const -> [Value] -> Value+ -- | A definition, which does not take effect until executed.+ -- The @Bool@ indicates whether the definition is recursive.+ VDef :: Bool -> Var -> Term -> Env -> Value+ -- | The result of a command, consisting of the result of the+ -- command as well as an environment of bindings from 'TDef'+ -- commands.+ VResult :: Value -> Env -> Value+ -- | An unevaluated bind expression, waiting to be executed, of the+ -- form /i.e./ @c1 ; c2@ or @x <- c1; c2@. We also store an 'Env'+ -- in which to interpret the commands.+ VBind :: Maybe Var -> Term -> Term -> Env -> Value+ -- | A (non-recursive) delayed term, along with its environment. If+ -- a term would otherwise be evaluated but we don't want it to be+ -- (/e.g./ as in the case of arguments to an 'if', or a recursive+ -- binding), we can stick a 'TDelay' on it, which turns it into a+ -- value. Delayed terms won't be evaluated until 'Force' is+ -- applied to them.+ VDelay :: Term -> Env -> Value+ -- | A reference to a memory cell in the store.+ VRef :: Int -> Value+ deriving (Eq, Show, Generic, FromJSON, ToJSON)++-- | Ensure that a value is not wrapped in 'VResult'.+stripVResult :: Value -> Value+stripVResult (VResult v _) = stripVResult v+stripVResult v = v++-- | Pretty-print a value.+prettyValue :: Value -> Text+prettyValue = prettyText . valueToTerm++-- | Inject a value back into a term.+valueToTerm :: Value -> Term+valueToTerm VUnit = TUnit+valueToTerm (VInt n) = TInt n+valueToTerm (VText s) = TText s+valueToTerm (VDir d) = TDir d+valueToTerm (VBool b) = TBool b+valueToTerm (VRobot r) = TRobot r+valueToTerm (VInj s v) = TApp (TConst (bool Inl Inr s)) (valueToTerm v)+valueToTerm (VPair v1 v2) = TPair (valueToTerm v1) (valueToTerm v2)+valueToTerm (VClo x t e) =+ M.foldrWithKey+ (\y v -> TLet False y Nothing (valueToTerm v))+ (TLam x Nothing t)+ (M.restrictKeys (unCtx e) (S.delete x (setOf freeVarsV (Syntax' NoLoc t ()))))+valueToTerm (VCApp c vs) = foldl' TApp (TConst c) (reverse (map valueToTerm vs))+valueToTerm (VDef r x t _) = TDef r x Nothing t+valueToTerm (VResult v _) = valueToTerm v+valueToTerm (VBind mx c1 c2 _) = TBind mx c1 c2+valueToTerm (VDelay t _) = TDelay SimpleDelay t+valueToTerm (VRef n) = TRef n++-- | An environment is a mapping from variable names to values.+type Env = Ctx Value
src/Swarm/TUI/Attr.hs view
@@ -15,7 +15,7 @@ -- and not those used in the world map, to avoid abusing attributes. -- For example using the robot attribute to highlight some text. ----- The few attributes that we use for drawing the logo are an exeption.+-- The few attributes that we use for drawing the logo are an exception. module Swarm.TUI.Attr ( swarmAttrMap, worldAttributes,@@ -40,7 +40,9 @@ infoAttr, boldAttr, dimAttr,+ magentaAttr, cyanAttr,+ lightCyanAttr, yellowAttr, blueAttr, greenAttr,@@ -83,6 +85,8 @@ , (blueAttr, fg V.blue) , (yellowAttr, fg V.yellow) , (cyanAttr, fg V.cyan)+ , (lightCyanAttr, fg (V.rgbColor @Int 200 255 255))+ , (magentaAttr, fg V.magenta) , -- Default attribute (defAttr, V.defAttr) ]@@ -161,12 +165,14 @@ defAttr = attrName "def" -- | Some basic colors used in TUI.-redAttr, greenAttr, blueAttr, yellowAttr, cyanAttr :: AttrName+redAttr, greenAttr, blueAttr, yellowAttr, cyanAttr, lightCyanAttr, magentaAttr :: AttrName redAttr = attrName "red" greenAttr = attrName "green" blueAttr = attrName "blue" yellowAttr = attrName "yellow" cyanAttr = attrName "cyan"+lightCyanAttr = attrName "lightCyan"+magentaAttr = attrName "magenta" instance ToJSON AttrName where toJSON = toJSON . head . attrNameComponents
src/Swarm/TUI/Controller.hs view
@@ -1,5 +1,5 @@ {-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE PatternSynonyms #-}+{-# LANGUAGE QuasiQuotes #-} -- | -- Module : Swarm.TUI.Controller@@ -38,7 +38,8 @@ handleInfoPanelEvent, ) where -import Brick hiding (Direction)+import Brick hiding (Direction, Location)+import Brick qualified import Brick.Focus import Brick.Widgets.Dialog import Brick.Widgets.Edit (handleEditorEvent)@@ -49,10 +50,12 @@ import Control.Lens import Control.Lens.Extras (is) import Control.Monad.Except+import Control.Monad.Extra (whenJust) import Control.Monad.State import Data.Bits import Data.Either (isRight)-import Data.Int (Int64)+import Data.Foldable (toList)+import Data.Int (Int32) import Data.List.NonEmpty (NonEmpty (..)) import Data.List.NonEmpty qualified as NE import Data.Map qualified as M@@ -61,48 +64,50 @@ import Data.Text qualified as T import Data.Text.IO qualified as T import Data.Time (getZonedTime)+import Data.Vector qualified as V import Graphics.Vty qualified as V import Linear import Swarm.Game.CESK (cancel, emptyStore, initMachine) import Swarm.Game.Entity hiding (empty) import Swarm.Game.Robot+import Swarm.Game.Scenario.Objective.Presentation.Model+import Swarm.Game.Scenario.Objective.Presentation.Render qualified as GR import Swarm.Game.ScenarioInfo import Swarm.Game.State import Swarm.Game.Step (gameTick)-import Swarm.Game.Value (Value (VUnit), prettyValue) import Swarm.Game.World qualified as W import Swarm.Language.Capability (Capability (CMake)) import Swarm.Language.Context+import Swarm.Language.Module import Swarm.Language.Parse (reservedWords) import Swarm.Language.Pipeline+import Swarm.Language.Pipeline.QQ (tmQ) import Swarm.Language.Pretty import Swarm.Language.Requirement qualified as R import Swarm.Language.Syntax import Swarm.Language.Typed (Typed (..)) import Swarm.Language.Types+import Swarm.Language.Value (Value (VUnit), prettyValue, stripVResult)+import Swarm.TUI.Controller.Util import Swarm.TUI.Inventory.Sorting (cycleSortDirection, cycleSortOrder) import Swarm.TUI.List import Swarm.TUI.Model+import Swarm.TUI.Model.Achievement.Definitions+import Swarm.TUI.Model.Achievement.Persistence+import Swarm.TUI.Model.Name+import Swarm.TUI.Model.Repl+import Swarm.TUI.Model.StateUpdate+import Swarm.TUI.Model.UI import Swarm.TUI.View (generateModal) import Swarm.Util hiding ((<<.=))+import Swarm.Util.Location import Swarm.Version (NewReleaseFailure (..)) import System.Clock+import System.FilePath (splitDirectories) import Witch (into) --- | Pattern synonyms to simplify brick event handler-pattern Key :: V.Key -> BrickEvent n e-pattern Key k = VtyEvent (V.EvKey k [])--pattern CharKey, ControlKey, MetaKey :: Char -> BrickEvent n e-pattern CharKey c = VtyEvent (V.EvKey (V.KChar c) [])-pattern ControlKey c = VtyEvent (V.EvKey (V.KChar c) [V.MCtrl])-pattern MetaKey c = VtyEvent (V.EvKey (V.KChar c) [V.MMeta])--pattern EscapeKey :: BrickEvent n e-pattern EscapeKey = VtyEvent (V.EvKey V.KEsc [])--pattern FKey :: Int -> BrickEvent n e-pattern FKey c = VtyEvent (V.EvKey (V.KFun c) [])+tutorialsDirname :: FilePath+tutorialsDirname = "Tutorials" -- | The top-level event handler for the TUI. handleEvent :: BrickEvent Name AppEvent -> EventM Name AppState ()@@ -111,8 +116,8 @@ AppEvent (UpstreamVersion ev) -> do let logReleaseEvent l e = runtimeState . eventLog %= logEvent l ("Release", -7) (T.pack $ show e) case ev of- Left e@(FailedReleaseQuery _e) -> logReleaseEvent ErrorTrace e- Left e -> logReleaseEvent Said e+ Left e@(FailedReleaseQuery _e) -> logReleaseEvent (ErrorTrace Error) e+ Left e -> logReleaseEvent (ErrorTrace Warning) e Right _ -> pure () runtimeState . upstreamRelease .= ev e -> do@@ -128,6 +133,7 @@ MainMenu l -> handleMainMenuEvent l NewGameMenu l -> handleNewGameMenuEvent l MessagesMenu -> handleMainMessagesEvent+ AchievementsMenu l -> handleMainAchievementsEvent l AboutMenu -> pressAnyKey (MainMenu (mainMenu About)) -- | The event handler for the main menu.@@ -149,7 +155,7 @@ let tutorialCollection = getTutorials ss topMenu = BL.listFindBy- ((== "Tutorials") . scenarioItemName)+ ((== tutorialsDirname) . T.unpack . scenarioItemName) (mkScenarioList cheat ss) tutorialMenu = mkScenarioList cheat tutorialCollection menuStack = NE.fromList [tutorialMenu, topMenu]@@ -162,20 +168,23 @@ _ -> error "No first tutorial found!" _ -> error "No first tutorial found!" startGame firstTutorial Nothing+ Achievements -> uiState . uiMenu .= AchievementsMenu (BL.list AchievementList (V.fromList listAchievements) 1) Messages -> do runtimeState . eventLog . notificationsCount .= 0 uiState . uiMenu .= MessagesMenu- About -> uiState . uiMenu .= AboutMenu+ About -> do+ uiState . uiMenu .= AboutMenu+ attainAchievement $ GlobalAchievement LookedAtAboutScreen Quit -> halt CharKey 'q' -> halt- ControlKey 'q' -> halt+ ControlChar 'q' -> halt VtyEvent ev -> do menu' <- nestEventM' menu (handleListEvent ev) uiState . uiMenu .= MainMenu menu' _ -> continueWithoutRedraw getTutorials :: ScenarioCollection -> ScenarioCollection-getTutorials sc = case M.lookup "Tutorials" (scMap sc) of+getTutorials sc = case M.lookup tutorialsDirname (scMap sc) of Just (SICollection _ c) -> c _ -> error "No tutorials exist!" @@ -183,11 +192,26 @@ advanceMenu :: Menu -> Menu advanceMenu = _NewGameMenu . ix 0 %~ BL.listMoveDown +handleMainAchievementsEvent ::+ BL.List Name CategorizedAchievement ->+ BrickEvent Name AppEvent ->+ EventM Name AppState ()+handleMainAchievementsEvent l e = case e of+ Key V.KEsc -> returnToMainMenu+ CharKey 'q' -> returnToMainMenu+ ControlChar 'q' -> returnToMainMenu+ VtyEvent ev -> do+ l' <- nestEventM' l (handleListEvent ev)+ uiState . uiMenu .= AchievementsMenu l'+ _ -> continueWithoutRedraw+ where+ returnToMainMenu = uiState . uiMenu .= MainMenu (mainMenu Messages)+ handleMainMessagesEvent :: BrickEvent Name AppEvent -> EventM Name AppState () handleMainMessagesEvent = \case Key V.KEsc -> returnToMainMenu CharKey 'q' -> returnToMainMenu- ControlKey 'q' -> returnToMainMenu+ ControlChar 'q' -> returnToMainMenu _ -> return () where returnToMainMenu = uiState . uiMenu .= MainMenu (mainMenu Messages)@@ -203,7 +227,7 @@ uiState . uiMenu .= NewGameMenu (NE.cons (mkScenarioList cheat c) scenarioStack) Key V.KEsc -> exitNewGameMenu scenarioStack CharKey 'q' -> exitNewGameMenu scenarioStack- ControlKey 'q' -> halt+ ControlChar 'q' -> halt VtyEvent ev -> do menu' <- nestEventM' curMenu (handleListEvent ev) uiState . uiMenu .= NewGameMenu (menu' :| rest)@@ -211,7 +235,8 @@ exitNewGameMenu :: NonEmpty (BL.List Name ScenarioItem) -> EventM Name AppState () exitNewGameMenu stk = do- uiState . uiMenu+ uiState+ . uiMenu .= case snd (NE.uncons stk) of Nothing -> MainMenu (mainMenu NewGame) Just stk' -> NewGameMenu stk'@@ -231,19 +256,22 @@ | s ^. gameState . paused -> continueWithoutRedraw | otherwise -> runFrameUI -- ctrl-q works everywhere- ControlKey 'q' ->+ ControlChar 'q' -> case s ^. gameState . winCondition of- Won _ -> toggleModal WinModal+ WinConditions (Won _) _ -> toggleModal WinModal+ WinConditions (Unwinnable _) _ -> toggleModal LoseModal _ -> toggleModal QuitModal VtyEvent (V.EvResize _ _) -> invalidateCacheEntry WorldCache Key V.KEsc | isJust (s ^. uiState . uiError) -> uiState . uiError .= Nothing | Just m <- s ^. uiState . uiModal -> do- safeAutoUnpause- uiState . uiModal .= Nothing- -- message modal is not autopaused, so update notifications when leaving it- when (m ^. modalType == MessagesModal) $ do- gameState . lastSeenMessageTime .= s ^. gameState . ticks+ safeAutoUnpause+ uiState . uiModal .= Nothing+ -- message modal is not autopaused, so update notifications when leaving it+ case m ^. modalType of+ MessagesModal -> do+ gameState . lastSeenMessageTime .= s ^. gameState . ticks+ _ -> return () FKey 1 -> toggleModal HelpModal FKey 2 -> toggleModal RobotsModal FKey 3 | not (null (s ^. gameState . availableRecipes . notificationsContent)) -> do@@ -255,10 +283,11 @@ FKey 5 | not (null (s ^. gameState . messageNotifications . notificationsContent)) -> do toggleModal MessagesModal gameState . lastSeenMessageTime .= s ^. gameState . ticks- ControlKey 'g' -> case s ^. uiState . uiGoal of- Just g | g /= [] -> toggleModal (GoalModal g)- _ -> continueWithoutRedraw- MetaKey 'h' -> do+ ControlChar 'g' ->+ if hasAnythingToShow $ s ^. uiState . uiGoal . goalsContent+ then toggleModal GoalModal+ else continueWithoutRedraw+ MetaChar 'h' -> do t <- liftIO $ getTime Monotonic h <- use $ uiState . uiHideRobotsUntil if h >= t@@ -269,54 +298,57 @@ uiState . uiHideRobotsUntil .= t + TimeSpec 2 0 invalidateCacheEntry WorldCache -- pausing and stepping- ControlKey 'p' | isRunning -> safeTogglePause- ControlKey 'o' | isRunning -> do+ ControlChar 'p' | isRunning -> safeTogglePause+ ControlChar 'o' | isRunning -> do gameState . runStatus .= ManualPause runGameTickUI -- speed controls- ControlKey 'x' | isRunning -> modify $ adjustTPS (+)- ControlKey 'z' | isRunning -> modify $ adjustTPS (-)+ ControlChar 'x' | isRunning -> modify $ adjustTPS (+)+ ControlChar 'z' | isRunning -> modify $ adjustTPS (-) -- special keys that work on all panels- MetaKey 'w' -> setFocus WorldPanel- MetaKey 'e' -> setFocus RobotPanel- MetaKey 'r' -> setFocus REPLPanel- MetaKey 't' -> setFocus InfoPanel+ MetaChar 'w' -> setFocus WorldPanel+ MetaChar 'e' -> setFocus RobotPanel+ MetaChar 'r' -> setFocus REPLPanel+ MetaChar 't' -> setFocus InfoPanel -- pass keys on to modal event handler if a modal is open VtyEvent vev | isJust (s ^. uiState . uiModal) -> handleModalEvent vev -- toggle creative mode if in "cheat mode"- ControlKey 'v'+ ControlChar 'v' | s ^. uiState . uiCheatMode -> gameState . creativeMode %= not MouseDown n _ _ mouseLoc -> case n of- WorldPanel -> do+ FocusablePanel WorldPanel -> do mouseCoordsM <- Brick.zoom gameState (mouseLocToWorldCoords mouseLoc) uiState . uiWorldCursor .= mouseCoordsM- REPLInput -> do- setFocus REPLPanel- handleREPLEvent ev+ REPLInput -> handleREPLEvent ev _ -> continueWithoutRedraw MouseUp n _ _mouseLoc -> do case n of InventoryListItem pos -> uiState . uiInventory . traverse . _2 %= BL.listMoveTo pos _ -> return ()- setFocus $ case n of+ flip whenJust setFocus $ case n of -- Adapt click event origin to their right panel. -- For the REPL and the World view, using 'Brick.Widgets.Core.clickable' correctly set the origin. -- However this does not seems to work for the robot and info panel. -- Thus we force the destination focus here.- InventoryList -> RobotPanel- InventoryListItem _ -> RobotPanel- InfoViewport -> InfoPanel- _ -> n+ InventoryList -> Just RobotPanel+ InventoryListItem _ -> Just RobotPanel+ InfoViewport -> Just InfoPanel+ REPLInput -> Just REPLPanel+ _ -> Nothing+ case n of+ FocusablePanel x -> setFocus x+ _ -> return () -- dispatch any other events to the focused panel handler _ev -> do fring <- use $ uiState . uiFocusRing case focusGetCurrent fring of- Just REPLPanel -> handleREPLEvent ev- Just WorldPanel -> handleWorldEvent ev- Just RobotPanel -> handleRobotPanelEvent ev- Just InfoPanel -> handleInfoPanelEvent infoScroll ev+ Just (FocusablePanel x) -> ($ ev) $ case x of+ REPLPanel -> handleREPLEvent+ WorldPanel -> handleWorldEvent+ RobotPanel -> handleRobotPanelEvent+ InfoPanel -> handleInfoPanelEvent infoScroll _ -> continueWithoutRedraw mouseLocToWorldCoords :: Brick.Location -> EventM Name GameState (Maybe W.Coords)@@ -332,9 +364,6 @@ my = fst mouseLoc' + snd regionStart in pure . Just $ W.Coords (mx, my) -setFocus :: Name -> EventM Name AppState ()-setFocus name = uiState . uiFocusRing %= focusSetCurrent name- -- | Set the game to Running if it was (auto) paused otherwise to paused. -- -- Also resets the last frame time to now. If we are pausing, it@@ -360,39 +389,39 @@ toggleModal mt = do modal <- use $ uiState . uiModal case modal of- Nothing -> do- newModal <- gets $ flip generateModal mt- ensurePause- uiState . uiModal ?= newModal+ Nothing -> openModal mt Just _ -> uiState . uiModal .= Nothing >> safeAutoUnpause- where- -- Set the game to AutoPause if needed- ensurePause = do- pause <- use $ gameState . paused- unless (pause || isRunningModal mt) $ do- gameState . runStatus .= AutoPause --- | The running modals do not autopause the game.-isRunningModal :: ModalType -> Bool-isRunningModal mt = mt `elem` [RobotsModal, MessagesModal]- handleModalEvent :: V.Event -> EventM Name AppState () handleModalEvent = \case V.EvKey V.KEnter [] -> do mdialog <- preuse $ uiState . uiModal . _Just . modalDialog toggleModal QuitModal- case dialogSelection <$> mdialog of- Just (Just QuitButton) -> quitGame- Just (Just KeepPlayingButton) -> toggleModal KeepPlayingModal- Just (Just (StartOverButton currentSeed siPair)) -> restartGame currentSeed siPair- Just (Just (NextButton siPair)) -> saveScenarioInfoOnQuit >> startGame siPair Nothing+ case dialogSelection =<< mdialog of+ Just (Button QuitButton, _) -> quitGame+ Just (Button KeepPlayingButton, _) -> toggleModal KeepPlayingModal+ Just (Button StartOverButton, StartOver currentSeed siPair) -> restartGame currentSeed siPair+ Just (Button NextButton, Next siPair) -> saveScenarioInfoOnQuit >> startGame siPair Nothing _ -> return () ev -> do Brick.zoom (uiState . uiModal . _Just . modalDialog) (handleDialogEvent ev) modal <- preuse $ uiState . uiModal . _Just . modalType case modal of- Just _ -> handleInfoPanelEvent modalScroll (VtyEvent ev)+ Just GoalModal -> case ev of+ V.EvKey (V.KChar '\t') [] -> uiState . uiGoal . focus %= focusNext+ _ -> do+ focused <- use $ uiState . uiGoal . focus+ case focusGetCurrent focused of+ Just (GoalWidgets w) -> case w of+ ObjectivesList -> do+ lw <- use $ uiState . uiGoal . listWidget+ newList <- refreshList lw+ uiState . uiGoal . listWidget .= newList+ GoalSummary -> handleInfoPanelEvent modalScroll (VtyEvent ev)+ _ -> handleInfoPanelEvent modalScroll (VtyEvent ev) _ -> return ()+ where+ refreshList lw = nestEventM' lw $ handleListEventWithSeparators ev isHeader -- | Write the @ScenarioInfo@ out to disk when exiting a game. saveScenarioInfoOnQuit :: (MonadIO m, MonadState AppState m) => m ()@@ -408,7 +437,10 @@ gs <- use $ gameState . scenarios p <- liftIO $ normalizeScenarioPath gs p' t <- liftIO getZonedTime- won <- isJust <$> preuse (gameState . winCondition . _Won)+ wc <- use $ gameState . winCondition+ let won = case wc of+ WinConditions (Won _) _ -> True+ _ -> False ts <- use $ gameState . ticks let currentScenarioInfo :: Traversal' AppState ScenarioInfo currentScenarioInfo = gameState . scenarios . scenarioItemByPath p . _SISingle . _2@@ -416,7 +448,14 @@ status <- preuse currentScenarioInfo case status of Nothing -> return ()- Just si -> liftIO $ saveScenarioInfo p si+ Just si -> do+ let segments = splitDirectories p+ case segments of+ firstDir : _ -> do+ when (won && firstDir == tutorialsDirname) $+ attainAchievement' t (Just p) (GlobalAchievement CompletedSingleTutorial)+ _ -> return ()+ liftIO $ saveScenarioInfo p si -- See what scenario is currently focused in the menu. Depending on how the -- previous scenario ended (via quit vs. via win), it might be the same as@@ -556,12 +595,30 @@ gs' <- liftIO (Fused.runM (Fused.execState gs f)) gameState .= gs' +updateAchievements :: EventM Name AppState ()+updateAchievements = do+ -- Merge the in-game achievements with the master list in UIState+ achievementsFromGame <- use $ gameState . gameAchievements+ let wrappedGameAchievements = M.mapKeys GameplayAchievement achievementsFromGame++ oldMasterAchievementsList <- use $ uiState . uiAchievements+ uiState . uiAchievements %= M.unionWith (<>) wrappedGameAchievements++ -- Don't save to disk unless there was a change in the attainment list.+ let incrementalAchievements = wrappedGameAchievements `M.difference` oldMasterAchievementsList+ unless (null incrementalAchievements) $ do+ -- TODO: #916 This is where new achievements would be displayed in a popup+ newAchievements <- use $ uiState . uiAchievements+ liftIO $ saveAchievementsInfo $ M.elems newAchievements+ -- | Run the game for a single tick (/without/ updating the UI). -- Every robot is given a certain amount of maximum computation to -- perform a single world action (like moving, turning, grabbing, -- etc.). runGameTick :: EventM Name AppState ()-runGameTick = zoomGameState gameTick+runGameTick = do+ zoomGameState gameTick+ updateAchievements -- | Update the UI. This function is used after running the -- game for some number of ticks.@@ -605,7 +662,7 @@ -- result as a REPL output, with its type, and reset the replStatus. REPLWorking (Typed (Just v) pty reqs) -> do let finalType = stripCmd pty- let val = Typed v finalType reqs+ let val = Typed (stripVResult v) finalType reqs itIx <- use (gameState . replNextValueIndex) let itName = fromString $ "it" ++ show itIx let out = T.intercalate " " [itName, ":", prettyText finalType, "=", into (prettyValue v)]@@ -618,19 +675,29 @@ -- Otherwise, do nothing. _ -> pure False - -- If the focused robot's log has been updated, attempt to- -- automatically switch to it and scroll all the way down so the new- -- message can be seen.+ -- If the focused robot's log has been updated and the UI focus+ -- isn't currently on the inventory or info panels, attempt to+ -- automatically switch to the logger and scroll all the way down so+ -- the new message can be seen. uiState . uiScrollToEnd .= False logUpdated <- do- case maybe False (view robotLogUpdated) fr of+ -- If the inventory or info panels are currently focused, it would+ -- be rude to update them right under the user's nose, so consider+ -- them "sticky". They will be updated as soon as the player moves+ -- the focus away.+ fring <- use $ uiState . uiFocusRing+ let sticky = focusGetCurrent fring `elem` map (Just . FocusablePanel) [RobotPanel, InfoPanel]++ -- Check if the robot log was updated and we are allowed to change+ -- the inventory+info panels.+ case maybe False (view robotLogUpdated) fr && not sticky of False -> pure False True -> do -- Reset the log updated flag zoomGameState clearFocusedRobotLogUpdated - -- Find and focus an installed "logger" device in the inventory list.- let isLogger (InstalledEntry e) = e ^. entityName == "logger"+ -- Find and focus an equipped "logger" device in the inventory list.+ let isLogger (EquippedEntry e) = e ^. entityName == "logger" isLogger _ = False focusLogger = BL.listFindBy isLogger @@ -658,35 +725,89 @@ oldBotMore <- uiState . uiMoreInfoBot <<.= botMore return $ oldTopMore /= topMore || oldBotMore /= botMore - -- Decide whether we need to update the current goal text, and pop+ goalOrWinUpdated <- doGoalUpdates++ let redraw = g ^. needsRedraw || inventoryUpdated || replUpdated || logUpdated || infoPanelUpdated || goalOrWinUpdated+ pure redraw++-- | Either pops up the updated Goals modal+-- or pops up the Congratulations (Win) modal, or pops+-- up the Condolences (Lose) modal.+-- The Win modal will take precendence if the player+-- has met the necessary conditions to win the game.+--+-- If the player chooses to "Keep Playing" from the Win modal, the+-- updated Goals will then immediately appear.+-- This is desirable for:+-- * feedback as to the final goal the player accomplished,+-- * as a summary of all of the goals of the game+-- * shows the player more "optional" goals they can continue to pursue+doGoalUpdates :: EventM Name AppState Bool+doGoalUpdates = do+ curGoal <- use (uiState . uiGoal . goalsContent)+ isCheating <- use (uiState . uiCheatMode)+ curWinCondition <- use (gameState . winCondition)+ announcementsSeq <- use (gameState . announcementQueue)+ let announcementsList = toList announcementsSeq++ -- Decide whether we need to update the current goal text and pop -- up a modal dialog.- curGoal <- use (uiState . uiGoal)- newGoal <-- preuse (gameState . winCondition . _WinConditions . _NonEmpty . _1 . objectiveGoal)+ case curWinCondition of+ NoWinCondition -> return False+ WinConditions (Unwinnable False) x -> do+ -- This clears the "flag" that the Lose dialog needs to pop up+ gameState . winCondition .= WinConditions (Unwinnable True) x+ openModal LoseModal - let goalUpdated = curGoal /= newGoal- when goalUpdated $ do- uiState . uiGoal .= newGoal- case newGoal of- Just goal | goal /= [] -> do- toggleModal (GoalModal goal)- _ -> return ()+ uiState . uiMenu %= advanceMenu+ return True+ WinConditions (Won False) x -> do+ -- This clears the "flag" that the Win dialog needs to pop up+ gameState . winCondition .= WinConditions (Won True) x+ openModal WinModal - -- Decide whether to show a pop-up modal congratulating the user on- -- successfully completing the current challenge.- winModalUpdated <- do- w <- use (gameState . winCondition)- case w of- Won False -> do- gameState . winCondition .= Won True- toggleModal WinModal- uiState . uiMenu %= advanceMenu- return True- _ -> return False+ uiState . uiMenu %= advanceMenu+ return True+ WinConditions _ oc -> do+ let newGoalTracking = GoalTracking announcementsList $ constructGoalMap isCheating oc+ -- The "uiGoal" field is intialized with empty members, so we know that+ -- this will be the first time showing it if it will be nonempty after previously+ -- being empty.+ isFirstGoalDisplay = hasAnythingToShow newGoalTracking && not (hasAnythingToShow curGoal)+ goalWasUpdated = isFirstGoalDisplay || not (null announcementsList) - let redraw = g ^. needsRedraw || inventoryUpdated || replUpdated || logUpdated || infoPanelUpdated || goalUpdated || winModalUpdated- pure redraw+ -- Decide whether to show a pop-up modal congratulating the user on+ -- successfully completing the current challenge.+ when goalWasUpdated $ do+ let hasMultiple = hasMultipleGoals newGoalTracking+ defaultFocus =+ if hasMultiple+ then ObjectivesList+ else GoalSummary + ring =+ focusRing $+ map GoalWidgets $+ if hasMultiple+ then listEnums+ else [GoalSummary]++ -- The "uiGoal" field is necessary at least to "persist" the data that is needed+ -- if the player chooses to later "recall" the goals dialog with CTRL+g.+ uiState . uiGoal+ .= GoalDisplay+ newGoalTracking+ (GR.makeListWidget newGoalTracking)+ (focusSetCurrent (GoalWidgets defaultFocus) ring)++ -- This clears the "flag" that indicate that the goals dialog needs to be+ -- automatically popped up.+ gameState . announcementQueue .= mempty++ openModal GoalModal++ return goalWasUpdated+ -- | Make sure all tiles covering the visible part of the world are -- loaded. loadVisibleRegion :: EventM Name AppState ()@@ -719,19 +840,86 @@ -- | Handle a user input event for the REPL. handleREPLEvent :: BrickEvent Name AppEvent -> EventM Name AppState ()-handleREPLEvent = \case- ControlKey 'c' -> do+handleREPLEvent x = do+ s <- get+ let repl = s ^. uiState . uiREPL+ controlMode = repl ^. replControlMode+ uinput = repl ^. replPromptText+ case x of+ MetaChar 'p' ->+ onlyCreative $ do+ if T.null uinput+ then uiState . uiREPL . replControlMode %= cycleEnum+ else uiState . uiError ?= "Please clear the REPL first."+ _ -> case controlMode of+ Typing -> handleREPLEventTyping x+ Piloting -> handleREPLEventPiloting x++-- | Handle a user "piloting" input event for the REPL.+handleREPLEventPiloting :: BrickEvent Name AppEvent -> EventM Name AppState ()+handleREPLEventPiloting x = case x of+ Key V.KUp -> inputCmd "move"+ Key V.KDown -> inputCmd "turn back"+ Key V.KLeft -> inputCmd "turn left"+ Key V.KRight -> inputCmd "turn right"+ ShiftKey V.KUp -> inputCmd "turn north"+ ShiftKey V.KDown -> inputCmd "turn south"+ ShiftKey V.KLeft -> inputCmd "turn west"+ ShiftKey V.KRight -> inputCmd "turn east"+ Key V.KDel -> inputCmd "selfdestruct"+ CharKey 'g' -> inputCmd "grab"+ CharKey 'h' -> inputCmd "harvest"+ CharKey 'd' -> inputCmd "drill forward"+ CharKey 's' -> inputCmd "scan forward"+ CharKey 'b' -> inputCmd "blocked"+ CharKey 'u' -> inputCmd "upload base"+ _ -> inputCmd "noop"+ where+ inputCmd cmdText = do+ uiState . uiREPL %= setCmd (cmdText <> ";")+ modify validateREPLForm+ handleREPLEventTyping $ Key V.KEnter++ setCmd nt repl =+ repl+ & replPromptText .~ nt+ & replPromptType .~ CmdPrompt []++-- | Handle a user input event for the REPL.+handleREPLEventTyping :: BrickEvent Name AppEvent -> EventM Name AppState ()+handleREPLEventTyping = \case+ ControlChar 'c' -> do gameState . baseRobot . machine %= cancel uiState . uiREPL . replPromptType .= CmdPrompt []+ uiState . uiREPL . replPromptText .= "" Key V.KEnter -> do s <- get let topCtx = topContext s repl = s ^. uiState . uiREPL uinput = repl ^. replPromptText - startBaseProgram t@(ProcessedTerm _ (Module ty _) reqs _) =- (gameState . replStatus .~ REPLWorking (Typed Nothing ty reqs))+ -- The player typed something at the REPL and hit Enter; this+ -- function takes the resulting ProcessedTerm (if the REPL+ -- input is valid) and sets up the base robot to run it.+ startBaseProgram t@(ProcessedTerm (Module tm _) reqs reqCtx) =+ -- Set the REPL status to Working+ (gameState . replStatus .~ REPLWorking (Typed Nothing (tm ^. sType) reqs))+ -- The `reqCtx` maps names of variables defined in the+ -- term (by `def` statements) to their requirements.+ -- E.g. if we had `def m = move end`, the reqCtx would+ -- record the fact that `m` needs the `move` capability.+ -- We simply add the entire `reqCtx` to the robot's+ -- context, so we can look up requirements if we later+ -- need to requirements-check an argument to `build` or+ -- `reprogram` at runtime. See the discussion at+ -- https://github.com/swarm-game/swarm/pull/827 for more+ -- details.+ . (gameState . baseRobot . robotContext . defReqs <>~ reqCtx)+ -- Set up the robot's CESK machine to evaluate/execute the+ -- given term, being sure to initialize the CESK machine+ -- environment and store from the top-level context. . (gameState . baseRobot . machine .~ initMachine t (topCtx ^. defVals) (topCtx ^. defStore))+ -- Finally, be sure to activate the base robot. . (gameState %~ execState (activateRobot 0)) if not $ s ^. gameState . replWorking@@ -749,12 +937,12 @@ Just found | T.null uinput -> uiState %= resetREPL "" (CmdPrompt []) | otherwise -> do- uiState %= resetREPL found (CmdPrompt [])- modify validateREPLForm+ uiState %= resetREPL found (CmdPrompt [])+ modify validateREPLForm else continueWithoutRedraw Key V.KUp -> modify $ adjReplHistIndex Older Key V.KDown -> modify $ adjReplHistIndex Newer- ControlKey 'r' -> do+ ControlChar 'r' -> do s <- get let uinput = s ^. uiState . uiREPL . replPromptText case s ^. uiState . uiREPL . replPromptType of@@ -773,7 +961,7 @@ CmdPrompt {} -> continueWithoutRedraw SearchPrompt _ -> uiState %= resetREPL "" (CmdPrompt [])- ControlKey 'd' -> do+ ControlChar 'd' -> do text <- use $ uiState . uiREPL . replPromptText if text == T.empty then toggleModal QuitModal@@ -816,12 +1004,12 @@ -- the CmdPrompt, so when Tab is pressed again, this case then gets executed and -- repopulates them. | otherwise -> case candidateMatches of- [] -> setCmd t []- [m] -> setCmd (completeWith m) []- -- Perform completion with the first candidate, then populate the list- -- of all candidates with the current completion moved to the back- -- of the queue.- (m : ms) -> setCmd (completeWith m) (ms ++ [m])+ [] -> setCmd t []+ [m] -> setCmd (completeWith m) []+ -- Perform completion with the first candidate, then populate the list+ -- of all candidates with the current completion moved to the back+ -- of the queue.+ (m : ms) -> setCmd (completeWith m) (ms ++ [m]) where -- checks the "parity" of the number of quotes. If odd, then there is an open quote. hasOpenQuotes = (== 1) . (`mod` 2) . T.count "\""@@ -857,17 +1045,17 @@ case replPrompt of CmdPrompt _ | T.null uinput ->- let theType = s ^. gameState . replStatus . replActiveType- in s & uiState . uiREPL . replType .~ theType+ let theType = s ^. gameState . replStatus . replActiveType+ in s & uiState . uiREPL . replType .~ theType CmdPrompt _ | otherwise ->- let result = processTerm' (topCtx ^. defTypes) (topCtx ^. defReqs) uinput- theType = case result of- Right (Just (ProcessedTerm _ (Module ty _) _ _)) -> Just ty- _ -> Nothing- in s- & uiState . uiREPL . replValid .~ isRight result- & uiState . uiREPL . replType .~ theType+ let result = processTerm' (topCtx ^. defTypes) (topCtx ^. defReqs) uinput+ theType = case result of+ Right (Just (ProcessedTerm (Module tm _) _ _)) -> Just (tm ^. sType)+ _ -> Nothing+ in s+ & uiState . uiREPL . replValid .~ isRight result+ & uiState . uiREPL . replType .~ theType SearchPrompt _ -> s where uinput = s ^. uiState . uiREPL . replPromptText@@ -902,14 +1090,19 @@ -- World events ------------------------------------------------------------ -worldScrollDist :: Int64+worldScrollDist :: Int32 worldScrollDist = 8 +onlyCreative :: MonadState AppState m => m () -> m ()+onlyCreative a = do+ c <- use $ gameState . creativeMode+ when c a+ -- | Handle a user input event in the world view panel. handleWorldEvent :: BrickEvent Name AppEvent -> EventM Name AppState () -- scrolling the world view in Creative mode handleWorldEvent = \case- Key k | k `elem` moveKeys -> onlyCreative $ scrollView (^+^ (worldScrollDist *^ keyToDir k))+ Key k | k `elem` moveKeys -> onlyCreative $ scrollView (.+^ (worldScrollDist *^ keyToDir k)) CharKey 'c' -> do invalidateCacheEntry WorldCache gameState . viewCenterRule .= VCRobot 0@@ -918,9 +1111,6 @@ -- Fall-through case: don't do anything. _ -> continueWithoutRedraw where- onlyCreative a = do- c <- use $ gameState . creativeMode- when c a moveKeys = [ V.KUp , V.KDown@@ -933,7 +1123,7 @@ ] -- | Manually scroll the world view.-scrollView :: (V2 Int64 -> V2 Int64) -> EventM Name AppState ()+scrollView :: (Location -> Location) -> EventM Name AppState () scrollView update = do -- Manually invalidate the 'WorldCache' instead of just setting -- 'needsRedraw'. I don't quite understand why the latter doesn't@@ -943,7 +1133,7 @@ gameState %= modifyViewCenter update -- | Convert a directional key into a direction.-keyToDir :: V.Key -> V2 Int64+keyToDir :: V.Key -> Heading keyToDir V.KUp = north keyToDir V.KDown = south keyToDir V.KRight = east@@ -952,7 +1142,7 @@ keyToDir (V.KChar 'j') = south keyToDir (V.KChar 'k') = north keyToDir (V.KChar 'l') = east-keyToDir _ = V2 0 0+keyToDir _ = zero -- | Adjust the ticks per second speed. adjustTPS :: (Int -> Int -> Int) -> AppState -> AppState@@ -994,17 +1184,15 @@ makeEntity :: Entity -> EventM Name AppState () makeEntity e = do s <- get- let mkTy = PolyUnit- mkReq = R.singletonCap CMake- mkProg = TApp (TConst Make) (TText (e ^. entityName))- mkPT = ProcessedTerm mkProg (Module mkTy empty) mkReq empty+ let name = e ^. entityName+ mkPT = [tmQ| make $str:name |] topStore = fromMaybe emptyStore $ s ^? gameState . baseRobot . robotContext . defStore case isActive <$> (s ^? gameState . baseRobot) of Just False -> do- gameState . replStatus .= REPLWorking (Typed Nothing mkTy mkReq)+ gameState . replStatus .= REPLWorking (Typed Nothing PolyUnit (R.singletonCap CMake)) gameState . baseRobot . machine .= initMachine mkPT empty topStore gameState %= execState (activateRobot 0) _ -> continueWithoutRedraw
+ src/Swarm/TUI/Controller/Util.hs view
@@ -0,0 +1,53 @@+{-# LANGUAGE PatternSynonyms #-}++module Swarm.TUI.Controller.Util where++import Brick hiding (Direction)+import Brick.Focus+import Control.Lens+import Control.Monad (unless)+import Graphics.Vty qualified as V+import Swarm.Game.State+import Swarm.TUI.Model+import Swarm.TUI.Model.UI+import Swarm.TUI.View.Util (generateModal)++-- | Pattern synonyms to simplify brick event handler+pattern Key :: V.Key -> BrickEvent n e+pattern Key k = VtyEvent (V.EvKey k [])++pattern CharKey, ControlChar, MetaChar :: Char -> BrickEvent n e+pattern CharKey c = VtyEvent (V.EvKey (V.KChar c) [])+pattern ControlChar c = VtyEvent (V.EvKey (V.KChar c) [V.MCtrl])+pattern MetaChar c = VtyEvent (V.EvKey (V.KChar c) [V.MMeta])++pattern ShiftKey :: V.Key -> BrickEvent n e+pattern ShiftKey k = VtyEvent (V.EvKey k [V.MShift])++pattern EscapeKey :: BrickEvent n e+pattern EscapeKey = VtyEvent (V.EvKey V.KEsc [])++pattern FKey :: Int -> BrickEvent n e+pattern FKey c = VtyEvent (V.EvKey (V.KFun c) [])++openModal :: ModalType -> EventM Name AppState ()+openModal mt = do+ newModal <- gets $ flip generateModal mt+ ensurePause+ uiState . uiModal ?= newModal+ where+ -- Set the game to AutoPause if needed+ ensurePause = do+ pause <- use $ gameState . paused+ unless (pause || isRunningModal mt) $ do+ gameState . runStatus .= AutoPause++-- | The running modals do not autopause the game.+isRunningModal :: ModalType -> Bool+isRunningModal = \case+ RobotsModal -> True+ MessagesModal -> True+ _ -> False++setFocus :: FocusablePanel -> EventM Name AppState ()+setFocus name = uiState . uiFocusRing %= focusSetCurrent (FocusablePanel name)
src/Swarm/TUI/Model.hs view
@@ -16,11 +16,13 @@ -- * Custom UI label types -- $uilabel AppEvent (..),+ FocusablePanel (..), Name (..), -- * Menus and dialogs ModalType (..),- ButtonSelection (..),+ Button (..),+ ButtonAction (..), Modal (..), modalType, modalDialog,@@ -59,60 +61,21 @@ InventoryListEntry (..), _Separator, _InventoryEntry,- _InstalledEntry,-- -- ** UI Model- UIState,- uiMenu,- uiPlaying,- uiCheatMode,- uiFocusRing,- uiWorldCursor,- uiREPL,- uiInventory,- uiInventorySort,- uiMoreInfoTop,- uiMoreInfoBot,- uiScrollToEnd,- uiError,- uiModal,- uiGoal,- lgTicksPerSecond,- lastFrameTime,- accumulatedTime,- tickCount,- frameCount,- frameTickCount,- lastInfoTime,- uiShowFPS,- uiShowZero,- uiShowRobots,- uiHideRobotsUntil,- uiInventoryShouldUpdate,- uiTPF,- uiFPS,- scenarioRef,- appData,+ _EquippedEntry, -- *** REPL Panel Model REPLState,+ ReplControlMode (..), replPromptType, replPromptEditor, replPromptText, replValid, replLast, replType,+ replControlMode, replHistory, newREPLEditor, - -- ** Initialization- initFocusRing,- defaultPrompt,- initREPLState,- initLgTicksPerSecond,- initUIState,- lastEntry,- -- ** Updating populateInventoryList, infoScroll,@@ -126,19 +89,18 @@ logEvent, -- * App state- AppState,+ AppState (AppState), gameState, uiState, runtimeState, -- ** Initialization AppOpts (..),- initAppState,- startGame,- restartGame,- scenarioToAppState, Seed, + -- *** Re-exported types used in options+ ColorMode (..),+ -- ** Utility topContext, focusedItem,@@ -148,59 +110,32 @@ ) where import Brick-import Brick.Focus-import Brick.Widgets.Dialog (Dialog)-import Brick.Widgets.Edit (Editor, applyEdit, editorText, getEditContents) import Brick.Widgets.List qualified as BL-import Control.Applicative (Applicative (liftA2), (<|>)) import Control.Lens hiding (from, (<.>)) import Control.Monad.Except import Control.Monad.State-import Data.Bits (FiniteBits (finiteBitSize))-import Data.Foldable (toList) import Data.List (findIndex) import Data.List.NonEmpty (NonEmpty (..))-import Data.List.NonEmpty qualified as NE-import Data.Map (Map)-import Data.Map qualified as M-import Data.Maybe (fromMaybe, isJust)-import Data.Sequence (Seq)-import Data.Sequence qualified as Seq+import Data.Maybe (fromMaybe) import Data.Text (Text)-import Data.Text qualified as T-import Data.Text.Zipper qualified as TZ-import Data.Time (getZonedTime) import Data.Vector qualified as V import GitHash (GitInfo)+import Graphics.Vty (ColorMode (..)) import Linear (zero) import Network.Wai.Handler.Warp (Port) import Swarm.Game.Entity as E import Swarm.Game.Robot-import Swarm.Game.Scenario (loadScenario) import Swarm.Game.ScenarioInfo (- ScenarioCollection,- ScenarioInfo (..), ScenarioInfoPair,- ScenarioItem (..),- ScenarioStatus (..),- normalizeScenarioPath,- scMap,- scenarioCollectionToList,- scenarioItemByPath,- scenarioPath,- scenarioSolution,- scenarioStatus, _SISingle, ) import Swarm.Game.State-import Swarm.Game.World qualified as W-import Swarm.Language.Types import Swarm.TUI.Inventory.Sorting-import Swarm.Util+import Swarm.TUI.Model.Menu+import Swarm.TUI.Model.Name+import Swarm.TUI.Model.Repl+import Swarm.TUI.Model.UI import Swarm.Version (NewReleaseFailure (NoMainUpstreamRelease))-import System.Clock-import System.FilePath (dropTrailingPathSeparator, splitPath, takeFileName)-import Witch (into) ------------------------------------------------------------ -- Custom UI label types@@ -218,496 +153,12 @@ | UpstreamVersion (Either NewReleaseFailure String) deriving (Show) --- | 'Name' represents names to uniquely identify various components--- of the UI, such as forms, panels, caches, extents, and lists.-data Name- = -- | The panel containing the REPL.- REPLPanel- | -- | The panel containing the world view.- WorldPanel- | -- | The panel showing robot info and inventory on the top left.- RobotPanel- | -- | The info panel on the bottom left.- InfoPanel- | -- | The REPL input form.- REPLInput- | -- | The render cache for the world view.- WorldCache- | -- | The cached extent for the world view.- WorldExtent- | -- | The list of inventory items for the currently- -- focused robot.- InventoryList- | -- | The inventory item position in the InventoryList.- InventoryListItem Int- | -- | The list of main menu choices.- MenuList- | -- | The list of scenario choices.- ScenarioList- | -- | The scrollable viewport for the info panel.- InfoViewport- | -- | The scrollable viewport for any modal dialog.- ModalViewport- deriving (Eq, Ord, Show, Read)- infoScroll :: ViewportScroll Name infoScroll = viewportScroll InfoViewport modalScroll :: ViewportScroll Name modalScroll = viewportScroll ModalViewport ---------------------------------------------------------------- REPL History----------------------------------------------------------------- | An item in the REPL history.-data REPLHistItem- = -- | Something entered by the user.- REPLEntry Text- | -- | A response printed by the system.- REPLOutput Text- deriving (Eq, Ord, Show, Read)---- | Useful helper function to only get user input text.-getREPLEntry :: REPLHistItem -> Maybe Text-getREPLEntry = \case- REPLEntry t -> Just t- _ -> Nothing---- | Useful helper function to filter out REPL output.-isREPLEntry :: REPLHistItem -> Bool-isREPLEntry = isJust . getREPLEntry---- | Get the text of REPL input/output.-replItemText :: REPLHistItem -> Text-replItemText = \case- REPLEntry t -> t- REPLOutput t -> t---- | History of the REPL with indices (0 is first entry) to the current--- line and to the first entry since loading saved history.--- We also (ab)use the length of the REPL as the index of current--- input line, since that number is one past the index of last entry.-data REPLHistory = REPLHistory- { _replSeq :: Seq REPLHistItem- , _replIndex :: Int- , _replStart :: Int- }--makeLensesWith (lensRules & generateSignatures .~ False) ''REPLHistory---- | Sequence of REPL inputs and outputs, oldest entry is leftmost.-replSeq :: Lens' REPLHistory (Seq REPLHistItem)---- | The current index in the REPL history (if the user is going back--- through the history using up/down keys).-replIndex :: Lens' REPLHistory Int---- | The index of the first entry since loading saved history.------ It will be set on load and reset on save (happens during exit).-replStart :: Lens' REPLHistory Int---- | Create new REPL history (i.e. from loaded history file lines).-newREPLHistory :: [REPLHistItem] -> REPLHistory-newREPLHistory xs =- let s = Seq.fromList xs- in REPLHistory- { _replSeq = s- , _replStart = length s- , _replIndex = length s- }---- | Point the start of REPL history after current last line. See 'replStart'.-restartREPLHistory :: REPLHistory -> REPLHistory-restartREPLHistory h = h & replStart .~ replLength h---- | Current number lines of the REPL history - (ab)used as index of input buffer.-replLength :: REPLHistory -> Int-replLength = length . _replSeq---- | Add new REPL input - the index must have been pointing one past--- the last element already, so we increment it to keep it that way.-addREPLItem :: REPLHistItem -> REPLHistory -> REPLHistory-addREPLItem t h =- h- & replSeq %~ (|> t)- & replIndex .~ 1 + replLength h---- | Get the latest N items in history, starting with the oldest one.------ This is used to show previous REPL lines in UI, so we need the items--- sorted in the order they were entered and will be drawn top to bottom.-getLatestREPLHistoryItems :: Int -> REPLHistory -> [REPLHistItem]-getLatestREPLHistoryItems n h = toList latestN- where- latestN = Seq.drop oldestIndex $ h ^. replSeq- oldestIndex = max (h ^. replStart) $ length (h ^. replSeq) - n--data TimeDir = Newer | Older deriving (Eq, Ord, Show)--moveReplHistIndex :: TimeDir -> Text -> REPLHistory -> REPLHistory-moveReplHistIndex d lastEntered history = history & replIndex .~ newIndex- where- historyLen = replLength history- curText = fromMaybe lastEntered $ getCurrentItemText history- curIndex = history ^. replIndex- entries = history ^. replSeq- -- split repl at index- (olderP, newer) = Seq.splitAt curIndex entries- -- find first different entry in direction- notSameEntry = \case- REPLEntry t -> t /= curText- _ -> False- newIndex = case d of- Newer -> maybe historyLen (curIndex +) $ Seq.findIndexL notSameEntry newer- Older -> fromMaybe curIndex $ Seq.findIndexR notSameEntry olderP--getCurrentItemText :: REPLHistory -> Maybe Text-getCurrentItemText history = replItemText <$> Seq.lookup (history ^. replIndex) (history ^. replSeq)--replIndexIsAtInput :: REPLHistory -> Bool-replIndexIsAtInput repl = repl ^. replIndex == replLength repl---- | Given some text, removes the REPLEntry within REPLHistory which is equal to that.--- This is used when the user enters in search mode and want to traverse the history.--- If a command has been used many times, the history will be populated with it causing--- the effect that search command always finds the same command.-removeEntry :: Text -> REPLHistory -> REPLHistory-removeEntry foundtext hist = hist & replSeq %~ Seq.filter (/= REPLEntry foundtext)---- | Get the last REPLEntry in REPLHistory matching the given text-lastEntry :: Text -> REPLHistory -> Maybe Text-lastEntry t h =- case Seq.viewr $ Seq.filter matchEntry $ h ^. replSeq of- Seq.EmptyR -> Nothing- _ Seq.:> a -> Just (replItemText a)- where- matchesText histItem = t `T.isInfixOf` replItemText histItem- matchEntry = liftA2 (&&) matchesText isREPLEntry----------------------------------------------------------------- REPL----------------------------------------------------------------- | This data type tells us how to interpret the text typed--- by the player at the prompt (which is stored in Editor).-data REPLPrompt- = -- | Interpret the prompt text as a regular command.- -- The list is for potential completions, which we can- -- cycle through by hitting Tab repeatedly- CmdPrompt [Text]- | -- | Interpret the prompt text as "search this text in history"- SearchPrompt REPLHistory--defaultPrompt :: REPLPrompt-defaultPrompt = CmdPrompt []--data REPLState = REPLState- { _replPromptType :: REPLPrompt- , _replPromptEditor :: Editor Text Name- , _replValid :: Bool- , _replLast :: Text- , _replType :: Maybe Polytype- , _replHistory :: REPLHistory- }--newREPLEditor :: Text -> Editor Text Name-newREPLEditor t = applyEdit gotoEnd $ editorText REPLInput (Just 1) t- where- ls = T.lines t- pos = (length ls - 1, T.length (last ls))- gotoEnd = if null ls then id else TZ.moveCursor pos--initREPLState :: REPLHistory -> REPLState-initREPLState = REPLState defaultPrompt (newREPLEditor "") True "" Nothing--makeLensesWith (lensRules & generateSignatures .~ False) ''REPLState---- | The way we interpret text typed by the player in the REPL prompt.-replPromptType :: Lens' REPLState REPLPrompt---- | The prompt where the user can type input at the REPL.-replPromptEditor :: Lens' REPLState (Editor Text Name)---- | Convinience lens to get text from editor and replace it with new--- one that has the provided text.-replPromptText :: Lens' REPLState Text-replPromptText = lens g s- where- g r = r ^. replPromptEditor . to getEditContents . to T.concat- s r t = r & replPromptEditor .~ newREPLEditor t---- | Whether the prompt text is a valid 'Term'.-replValid :: Lens' REPLState Bool---- | The type of the current REPL input which should be displayed to--- the user (if any).-replType :: Lens' REPLState (Maybe Polytype)---- | The last thing the user has typed which isn't part of the history.--- This is used to restore the repl form after the user visited the history.-replLast :: Lens' REPLState Text---- | History of things the user has typed at the REPL, interleaved--- with outputs the system has generated.-replHistory :: Lens' REPLState REPLHistory----------------------------------------------------------------- Menus and dialogs---------------------------------------------------------------data ModalType- = HelpModal- | RecipesModal- | CommandsModal- | MessagesModal- | RobotsModal- | WinModal- | QuitModal- | KeepPlayingModal- | DescriptionModal Entity- | GoalModal [Text]- deriving (Eq, Show)--data ButtonSelection = CancelButton | KeepPlayingButton | StartOverButton Seed ScenarioInfoPair | QuitButton | NextButton ScenarioInfoPair--data Modal = Modal- { _modalType :: ModalType- , _modalDialog :: Dialog ButtonSelection- }--makeLenses ''Modal--data MainMenuEntry = NewGame | Tutorial | Messages | About | Quit- deriving (Eq, Ord, Show, Read, Bounded, Enum)--data Menu- = NoMenu -- We started playing directly from command line, no menu to show- | MainMenu (BL.List Name MainMenuEntry)- | NewGameMenu (NonEmpty (BL.List Name ScenarioItem)) -- stack of scenario item lists- | MessagesMenu- | AboutMenu--mainMenu :: MainMenuEntry -> BL.List Name MainMenuEntry-mainMenu e = BL.list MenuList (V.fromList [minBound .. maxBound]) 1 & BL.listMoveToElement e--makePrisms ''Menu---- | Create a brick 'BL.List' of scenario items from a 'ScenarioCollection'.-mkScenarioList :: Bool -> ScenarioCollection -> BL.List Name ScenarioItem-mkScenarioList cheat = flip (BL.list ScenarioList) 1 . V.fromList . filterTest . scenarioCollectionToList- where- filterTest = if cheat then id else filter (\case SICollection n _ -> n /= "Testing"; _ -> True)---- | Given a 'ScenarioCollection' and a 'FilePath' which is the canonical--- path to some folder or scenario, construct a 'NewGameMenu' stack--- focused on the given item, if possible.-mkNewGameMenu :: Bool -> ScenarioCollection -> FilePath -> Maybe Menu-mkNewGameMenu cheat sc path = NewGameMenu . NE.fromList <$> go (Just sc) (splitPath path) []- where- go :: Maybe ScenarioCollection -> [FilePath] -> [BL.List Name ScenarioItem] -> Maybe [BL.List Name ScenarioItem]- go _ [] stk = Just stk- go Nothing _ _ = Nothing- go (Just curSC) (thing : rest) stk = go nextSC rest (lst : stk)- where- hasName :: ScenarioItem -> Bool- hasName (SISingle (_, ScenarioInfo pth _ _ _)) = takeFileName pth == thing- hasName (SICollection nm _) = nm == into @Text (dropTrailingPathSeparator thing)-- lst = BL.listFindBy hasName (mkScenarioList cheat curSC)-- nextSC = case M.lookup (dropTrailingPathSeparator thing) (scMap curSC) of- Just (SICollection _ c) -> Just c- _ -> Nothing----------------------------------------------------------------- Inventory list entries----------------------------------------------------------------- | An entry in the inventory list displayed in the info panel. We--- can either have an entity with a count in the robot's inventory,--- an entity installed on the robot, or a labelled separator. The--- purpose of the separators is to show a clear distinction between--- the robot's /inventory/ and its /installed devices/.-data InventoryListEntry- = Separator Text- | InventoryEntry Count Entity- | InstalledEntry Entity- deriving (Eq)--makePrisms ''InventoryListEntry----------------------------------------------------------------- UI state----------------------------------------------------------------- | The main record holding the UI state. For access to the fields,--- see the lenses below.-data UIState = UIState- { _uiMenu :: Menu- , _uiPlaying :: Bool- , _uiCheatMode :: Bool- , _uiFocusRing :: FocusRing Name- , _uiWorldCursor :: Maybe W.Coords- , _uiREPL :: REPLState- , _uiInventory :: Maybe (Int, BL.List Name InventoryListEntry)- , _uiInventorySort :: InventorySortOptions- , _uiMoreInfoTop :: Bool- , _uiMoreInfoBot :: Bool- , _uiScrollToEnd :: Bool- , _uiError :: Maybe Text- , _uiModal :: Maybe Modal- , _uiGoal :: Maybe [Text]- , _uiShowFPS :: Bool- , _uiShowZero :: Bool- , _uiHideRobotsUntil :: TimeSpec- , _uiInventoryShouldUpdate :: Bool- , _uiTPF :: Double- , _uiFPS :: Double- , _lgTicksPerSecond :: Int- , _tickCount :: Int- , _frameCount :: Int- , _frameTickCount :: Int- , _lastFrameTime :: TimeSpec- , _accumulatedTime :: TimeSpec- , _lastInfoTime :: TimeSpec- , _appData :: Map Text Text- , _scenarioRef :: Maybe ScenarioInfoPair- }------------------------------------------------------- Lenses for UIState--let exclude = ['_lgTicksPerSecond]- in makeLensesWith- ( lensRules- & generateSignatures .~ False- & lensField . mapped . mapped %~ \fn n ->- if n `elem` exclude then [] else fn n- )- ''UIState---- | The current menu state.-uiMenu :: Lens' UIState Menu---- | Are we currently playing the game? True = we are playing, and--- should thus display a world, REPL, etc.; False = we should--- display the current menu.-uiPlaying :: Lens' UIState Bool---- | Cheat mode, i.e. are we allowed to turn creative mode on and off?-uiCheatMode :: Lens' UIState Bool---- | The focus ring is the set of UI panels we can cycle among using--- the Tab key.-uiFocusRing :: Lens' UIState (FocusRing Name)---- | The last clicked position on the world view.-uiWorldCursor :: Lens' UIState (Maybe W.Coords)---- | The state of REPL panel.-uiREPL :: Lens' UIState REPLState---- | The order and direction of sorting inventory list.-uiInventorySort :: Lens' UIState InventorySortOptions---- | The hash value of the focused robot entity (so we can tell if its--- inventory changed) along with a list of the items in the--- focused robot's inventory.-uiInventory :: Lens' UIState (Maybe (Int, BL.List Name InventoryListEntry))---- | Does the info panel contain more content past the top of the panel?-uiMoreInfoTop :: Lens' UIState Bool---- | Does the info panel contain more content past the bottom of the panel?-uiMoreInfoBot :: Lens' UIState Bool---- | A flag telling the UI to scroll the info panel to the very end--- (used when a new log message is appended).-uiScrollToEnd :: Lens' UIState Bool---- | When this is @Just@, it represents a popup box containing an--- error message that is shown on top of the rest of the UI.-uiError :: Lens' UIState (Maybe Text)---- | When this is @Just@, it represents a modal to be displayed on--- top of the UI, e.g. for the Help screen.-uiModal :: Lens' UIState (Maybe Modal)---- | Status of the scenario goal: whether there is one, and whether it--- has been displayed to the user initially.-uiGoal :: Lens' UIState (Maybe [Text])---- | A toggle to show the FPS by pressing `f`-uiShowFPS :: Lens' UIState Bool---- | A toggle to show or hide inventory items with count 0 by pressing `0`-uiShowZero :: Lens' UIState Bool---- | Hide robots on the world map.-uiHideRobotsUntil :: Lens' UIState TimeSpec---- | Whether to show or hide robots on the world map.-uiShowRobots :: Getter UIState Bool-uiShowRobots = to (\ui -> ui ^. lastFrameTime > ui ^. uiHideRobotsUntil)---- | Whether the Inventory ui panel should update-uiInventoryShouldUpdate :: Lens' UIState Bool---- | Computed ticks per milli seconds-uiTPF :: Lens' UIState Double---- | Computed frames per milli seconds-uiFPS :: Lens' UIState Double---- | The currently active Scenario description, useful for starting over.-scenarioRef :: Lens' UIState (Maybe ScenarioInfoPair)---- | The base-2 logarithm of the current game speed in ticks/second.--- Note that we cap this value to the range of +/- log2 INTMAX.-lgTicksPerSecond :: Lens' UIState Int-lgTicksPerSecond = lens _lgTicksPerSecond safeSetLgTicks- where- maxLog = finiteBitSize (maxBound :: Int)- maxTicks = maxLog - 2- minTicks = 2 - maxLog- safeSetLgTicks ui lTicks- | lTicks < minTicks = setLgTicks ui minTicks- | lTicks > maxTicks = setLgTicks ui maxTicks- | otherwise = setLgTicks ui lTicks- setLgTicks ui lTicks = ui {_lgTicksPerSecond = lTicks}---- | A counter used to track how many ticks have happened since the--- last time we updated the ticks/frame statistics.-tickCount :: Lens' UIState Int---- | A counter used to track how many frames have been rendered since the--- last time we updated the ticks/frame statistics.-frameCount :: Lens' UIState Int---- | A counter used to track how many ticks have happened in the--- current frame, so we can stop when we get to the tick cap.-frameTickCount :: Lens' UIState Int---- | The time of the last info widget update-lastInfoTime :: Lens' UIState TimeSpec---- | The time of the last 'Frame' event.-lastFrameTime :: Lens' UIState TimeSpec---- | The amount of accumulated real time. Every time we get a 'Frame'--- event, we accumulate the amount of real time that happened since--- the last frame, then attempt to take an appropriate number of--- ticks to "catch up", based on the target tick rate.------ See https://gafferongames.com/post/fix_your_timestep/ .-accumulatedTime :: Lens' UIState TimeSpec---- | Free-form data loaded from the @data@ directory, for things like--- the logo, about page, tutorial story, etc.-appData :: Lens' UIState (Map Text Text)- -- ---------------------------------------------------------------------------- -- Runtime state -- -- ----------------------------------------------------------------------------@@ -792,68 +243,13 @@ -- | Get the currently focused entity from the robot info panel (if -- any). This is just like 'focusedItem' but forgets the--- distinction between plain inventory items and installed devices.+-- distinction between plain inventory items and equipped devices. focusedEntity :: AppState -> Maybe Entity focusedEntity = focusedItem >=> \case Separator _ -> Nothing InventoryEntry _ e -> Just e- InstalledEntry e -> Just e------------------------------------------------------- UIState initialization---- | The initial state of the focus ring.-initFocusRing :: FocusRing Name-initFocusRing = focusRing [REPLPanel, InfoPanel, RobotPanel, WorldPanel]---- | The initial tick speed.-initLgTicksPerSecond :: Int-initLgTicksPerSecond = 4 -- 2^4 = 16 ticks / second---- | Initialize the UI state. This needs to be in the IO monad since--- it involves reading a REPL history file, getting the current--- time, and loading text files from the data directory. The @Bool@--- parameter indicates whether we should start off by showing the--- main menu.-initUIState :: Bool -> Bool -> ExceptT Text IO UIState-initUIState showMainMenu cheatMode = liftIO $ do- historyT <- readFileMayT =<< getSwarmHistoryPath False- appDataMap <- readAppData- let history = maybe [] (map REPLEntry . T.lines) historyT- startTime <- getTime Monotonic- return $- UIState- { _uiMenu = if showMainMenu then MainMenu (mainMenu NewGame) else NoMenu- , _uiPlaying = not showMainMenu- , _uiCheatMode = cheatMode- , _uiFocusRing = initFocusRing- , _uiWorldCursor = Nothing- , _uiREPL = initREPLState $ newREPLHistory history- , _uiInventory = Nothing- , _uiInventorySort = defaultSortOptions- , _uiMoreInfoTop = False- , _uiMoreInfoBot = False- , _uiScrollToEnd = False- , _uiError = Nothing- , _uiModal = Nothing- , _uiGoal = Nothing- , _uiShowFPS = False- , _uiShowZero = True- , _uiHideRobotsUntil = startTime - 1- , _uiInventoryShouldUpdate = False- , _uiTPF = 0- , _uiFPS = 0- , _lgTicksPerSecond = initLgTicksPerSecond- , _lastFrameTime = startTime- , _accumulatedTime = 0- , _lastInfoTime = 0- , _tickCount = 0- , _frameCount = 0- , _frameTickCount = 0- , _appData = appDataMap- , _scenarioRef = Nothing- }+ EquippedEntry e -> Just e ------------------------------------------------------------ -- Functions for updating the UI state@@ -868,23 +264,27 @@ showZero <- use uiShowZero sortOptions <- use uiInventorySort let mkInvEntry (n, e) = InventoryEntry n e- mkInstEntry (_, e) = InstalledEntry e- itemList mk label =+ mkInstEntry (_, e) = EquippedEntry e+ itemList isInventoryDisplay mk label = (\case [] -> []; xs -> Separator label : xs) . map mk . sortInventory sortOptions . filter shouldDisplay . elems-- -- Display items if we have a positive number of them, or they- -- aren't an installed device. In other words we don't need to- -- display installed devices twice unless we actually have some- -- in our inventory in addition to being installed.- shouldDisplay (n, e) = n > 0 || showZero && not ((r ^. installedDevices) `E.contains` e)+ where+ -- Display items if we have a positive number of them, or they+ -- aren't an equipped device. In other words we don't need to+ -- display equipped devices twice unless we actually have some+ -- in our inventory in addition to being equipped.+ shouldDisplay (n, e) =+ n > 0+ || isInventoryDisplay+ && showZero+ && not ((r ^. equippedDevices) `E.contains` e) items =- (r ^. robotInventory . to (itemList mkInvEntry "Inventory"))- ++ (r ^. installedDevices . to (itemList mkInstEntry "Installed devices"))+ (r ^. robotInventory . to (itemList True mkInvEntry "Inventory"))+ ++ (r ^. equippedDevices . to (itemList False mkInstEntry "Equipped devices")) -- Attempt to keep the selected element steady. sel = mList >>= BL.listSelectedElement -- Get the currently selected element+index.@@ -896,8 +296,8 @@ -- if it's not there, keep the index the same. Just (selIdx, InventoryEntry _ e) -> fromMaybe selIdx (findIndex ((== Just e) . preview (_InventoryEntry . _2)) items)- Just (selIdx, InstalledEntry e) ->- fromMaybe selIdx (findIndex ((== Just e) . preview _InstalledEntry) items)+ Just (selIdx, EquippedEntry e) ->+ fromMaybe selIdx (findIndex ((== Just e) . preview _EquippedEntry) items) Just (selIdx, _) -> selIdx -- Create the new list, focused at the desired index.@@ -913,72 +313,24 @@ -- | Command-line options for configuring the app. data AppOpts = AppOpts- { -- | Explicit seed chosen by the user.- userSeed :: Maybe Seed- , -- | Scenario the user wants to play.- userScenario :: Maybe FilePath- , -- | Code to be run on base.- scriptToRun :: Maybe FilePath- , -- | Automatically run the solution defined in the scenario file- autoPlay :: Bool- , -- | Should cheat mode be enabled?- cheatMode :: Bool- , -- | Explicit port on which to run the web API- userWebPort :: Maybe Port- , -- | Information about the Git repository (not present in release).- repoGitInfo :: Maybe GitInfo+ { userSeed :: Maybe Seed+ -- ^ Explicit seed chosen by the user.+ , userScenario :: Maybe FilePath+ -- ^ Scenario the user wants to play.+ , scriptToRun :: Maybe FilePath+ -- ^ Code to be run on base.+ , autoPlay :: Bool+ -- ^ Automatically run the solution defined in the scenario file+ , cheatMode :: Bool+ -- ^ Should cheat mode be enabled?+ , colorMode :: Maybe ColorMode+ -- ^ What colour mode should be used?+ , userWebPort :: Maybe Port+ -- ^ Explicit port on which to run the web API+ , repoGitInfo :: Maybe GitInfo+ -- ^ Information about the Git repository (not present in release). } --- | Initialize the 'AppState'.-initAppState :: AppOpts -> ExceptT Text IO AppState-initAppState AppOpts {..} = do- let isRunningInitialProgram = isJust scriptToRun || autoPlay- skipMenu = isJust userScenario || isRunningInitialProgram || isJust userSeed- gs <- initGameState- ui <- initUIState (not skipMenu) cheatMode- let rs = initRuntimeState- case skipMenu of- False -> return $ AppState gs ui rs- True -> do- (scenario, path) <- loadScenario (fromMaybe "classic" userScenario) (gs ^. entityMap)-- let maybeAutoplay = do- guard autoPlay- soln <- scenario ^. scenarioSolution- return $ SuggestedSolution soln- let realToRun = maybeAutoplay <|> (ScriptPath <$> scriptToRun)-- execStateT- (startGameWithSeed userSeed (scenario, ScenarioInfo path NotStarted NotStarted NotStarted) realToRun)- (AppState gs ui rs)---- | Load a 'Scenario' and start playing the game.-startGame :: (MonadIO m, MonadState AppState m) => ScenarioInfoPair -> Maybe CodeToRun -> m ()-startGame = startGameWithSeed Nothing---- | Re-initialize the game from the stored reference to the current scenario.------ Note that "restarting" is intended only for "scenarios";--- with some scenarios, it may be possible to get stuck so that it is--- either impossible or very annoying to win, so being offered an--- option to restart is more user-friendly.------ Since scenarios are stored as a Maybe in the UI state, we handle the Nothing--- case upstream so that the Scenario passed to this function definitely exists.-restartGame :: (MonadIO m, MonadState AppState m) => Seed -> ScenarioInfoPair -> m ()-restartGame currentSeed siPair = startGameWithSeed (Just currentSeed) siPair Nothing---- | Load a 'Scenario' and start playing the game, with the--- possibility for the user to override the seed.-startGameWithSeed :: (MonadIO m, MonadState AppState m) => Maybe Seed -> ScenarioInfoPair -> Maybe CodeToRun -> m ()-startGameWithSeed userSeed siPair@(_scene, si) toRun = do- t <- liftIO getZonedTime- ss <- use $ gameState . scenarios- p <- liftIO $ normalizeScenarioPath ss (si ^. scenarioPath)- gameState . currentScenarioPath .= Just p- gameState . scenarios . scenarioItemByPath p . _SISingle . _2 . scenarioStatus .= InProgress t 0 0- scenarioToAppState siPair userSeed toRun- -- | Extract the scenario which would come next in the menu from the -- currently selected scenario (if any). Can return @Nothing@ if -- either we are not in the @NewGameMenu@, or the current scenario@@ -1005,34 +357,3 @@ ctxPossiblyWithIt = case s ^. gameState . replStatus of REPLDone (Just p) -> ctx & at "it" ?~ p _ -> ctx---- XXX do we need to keep an old entity map around???---- | Modify the 'AppState' appropriately when starting a new scenario.-scenarioToAppState :: (MonadIO m, MonadState AppState m) => ScenarioInfoPair -> Maybe Seed -> Maybe CodeToRun -> m ()-scenarioToAppState siPair@(scene, _) userSeed toRun = do- withLensIO gameState $ scenarioToGameState scene userSeed toRun- withLensIO uiState $ scenarioToUIState siPair- where- withLensIO :: (MonadIO m, MonadState AppState m) => Lens' AppState x -> (x -> IO x) -> m ()- withLensIO l a = do- x <- use l- x' <- liftIO $ a x- l .= x'---- | Modify the UI state appropriately when starting a new scenario.-scenarioToUIState :: ScenarioInfoPair -> UIState -> IO UIState-scenarioToUIState siPair u =- return $- u- & uiPlaying .~ True- & uiGoal .~ Nothing- & uiFocusRing .~ initFocusRing- & uiInventory .~ Nothing- & uiInventorySort .~ defaultSortOptions- & uiShowFPS .~ False- & uiShowZero .~ True- & lgTicksPerSecond .~ initLgTicksPerSecond- & uiREPL .~ initREPLState (u ^. uiREPL . replHistory)- & uiREPL . replHistory %~ restartREPLHistory- & scenarioRef ?~ siPair
+ src/Swarm/TUI/Model/Achievement/Attainment.hs view
@@ -0,0 +1,47 @@+{-# LANGUAGE TemplateHaskell #-}++module Swarm.TUI.Model.Achievement.Attainment where++import Control.Lens hiding (from, (<.>))+import Data.Aeson (+ Options (..),+ defaultOptions,+ genericParseJSON,+ genericToJSON,+ )+import Data.Function (on)+import Data.Time (ZonedTime, zonedTimeToUTC)+import Data.Yaml as Y+import GHC.Generics (Generic)+import Swarm.TUI.Model.Achievement.Definitions++data Attainment = Attainment+ { _achievement :: CategorizedAchievement+ , _maybeScenarioPath :: Maybe FilePath+ -- ^ from which scenario was it obtained?+ , _obtainedAt :: ZonedTime+ }+ deriving (Generic)++makeLenses ''Attainment++instance Eq Attainment where+ (==) = (==) `on` _achievement++instance Ord Attainment where+ compare = compare `on` (zonedTimeToUTC . _obtainedAt)++instance Semigroup Attainment where+ (<>) = min++instance FromJSON Attainment where+ parseJSON = genericParseJSON achievementJsonOptions++instance ToJSON Attainment where+ toJSON = genericToJSON achievementJsonOptions++achievementJsonOptions :: Options+achievementJsonOptions =+ defaultOptions+ { fieldLabelModifier = tail -- drops leading underscore+ }
+ src/Swarm/TUI/Model/Achievement/Definitions.hs view
@@ -0,0 +1,89 @@+module Swarm.TUI.Model.Achievement.Definitions where++import Data.Aeson+import Data.Text (Text)+import GHC.Generics (Generic)+import Swarm.Util++data ExpectedEffort+ = Trivial+ | Easy+ | Moderate+ | Gruelling+ deriving (Eq, Ord, Show, Bounded, Enum, Generic, FromJSON, ToJSON)++data Quotation = Quotation+ { attribution :: Text+ , content :: Text+ }+ deriving (Eq, Show, Generic, FromJSON, ToJSON)++data FlavorText+ = Freeform Text+ | FTQuotation Quotation+ deriving (Eq, Show, Generic, FromJSON, ToJSON)++data AchievementInfo = AchievementInfo+ { title :: Text+ -- ^ Guidelines:+ -- * prefer puns, pop culture references, etc.+ -- * should be a phrase in Title Case.+ -- * For achievements that are "obfuscated", this can be+ -- a vague "clue" as to what the attainment entails.+ , humorousElaboration :: Maybe FlavorText+ -- ^ Explain the reference, e.g. in the form of a full quote+ -- from a movie, or something you might find+ -- in a fortune cookie+ , attainmentProcess :: Text+ -- ^ Precisely what must be done to obtain this achievement.+ , effort :: ExpectedEffort+ , isObfuscated :: Bool+ -- ^ Hides the attainment process until after the achievement is attained.+ -- Best when the title + elaboration constitute a good clue.+ }+ deriving (Eq, Show, Generic, FromJSON, ToJSON)++data CategorizedAchievement+ = GlobalAchievement GlobalAchievement+ | GameplayAchievement GameplayAchievement+ deriving (Eq, Ord, Show, Generic)++categorizedAchievementJsonOptions :: Options+categorizedAchievementJsonOptions =+ defaultOptions+ { sumEncoding = UntaggedValue+ }++instance ToJSON CategorizedAchievement where+ toJSON = genericToJSON categorizedAchievementJsonOptions++instance FromJSON CategorizedAchievement where+ parseJSON = genericParseJSON categorizedAchievementJsonOptions++-- | Achievements that entail some aggregate of actions+-- across scenarios+data GlobalAchievement+ = CompletedSingleTutorial+ | CompletedAllTutorials+ | LookedAtAboutScreen+ deriving (Eq, Ord, Show, Bounded, Enum, Generic)++instance FromJSON GlobalAchievement+instance ToJSON GlobalAchievement++-- | Achievements obtained while playing a single scenario+data GameplayAchievement+ = CraftedBitcoin+ | RobotIntoWater+ | AttemptSelfDestructBase+ | DestroyedBase+ | LoseScenario+ deriving (Eq, Ord, Show, Bounded, Enum, Generic)++instance FromJSON GameplayAchievement+instance ToJSON GameplayAchievement++listAchievements :: [CategorizedAchievement]+listAchievements =+ map GlobalAchievement listEnums+ <> map GameplayAchievement listEnums
+ src/Swarm/TUI/Model/Achievement/Description.hs view
@@ -0,0 +1,70 @@+{-# LANGUAGE OverloadedStrings #-}++module Swarm.TUI.Model.Achievement.Description where++import Swarm.TUI.Model.Achievement.Definitions++describe :: CategorizedAchievement -> AchievementInfo+describe (GlobalAchievement CompletedSingleTutorial) =+ AchievementInfo+ "Welcome Freshmen"+ (Just $ Freeform "School is in session!")+ "Complete one of the tutorials."+ Easy+ False+describe (GlobalAchievement CompletedAllTutorials) =+ AchievementInfo+ "Autodidact"+ ( Just $+ FTQuotation $+ Quotation+ "Terry Pratchet"+ "I didn't go to university... But I have sympathy for those who did."+ )+ "Complete all of the tutorials."+ Moderate+ False+describe (GlobalAchievement LookedAtAboutScreen) =+ AchievementInfo+ "About time!"+ Nothing+ "View the About screen."+ Trivial+ True+describe (GameplayAchievement CraftedBitcoin) =+ -- Bitcoin is the deepest level of the recipes+ -- hierarchy.+ AchievementInfo+ "Master of Your Craft"+ Nothing+ "Make a Bitcoin"+ Moderate+ True+describe (GameplayAchievement RobotIntoWater) =+ AchievementInfo+ "Watery Grave"+ (Just $ Freeform "This little robot thinks he's a submarine.")+ "Destroy a robot by sending it into the water."+ Easy+ True+describe (GameplayAchievement AttemptSelfDestructBase) =+ AchievementInfo+ "Call of the Void"+ (Just $ Freeform "What does that big red button do?")+ "Attempt to self-destruct your base."+ Easy+ True+describe (GameplayAchievement DestroyedBase) =+ AchievementInfo+ "That Could Have Gone Better"+ (Just $ Freeform "Boom.")+ "Actually destroy your base."+ Moderate+ True+describe (GameplayAchievement LoseScenario) =+ AchievementInfo+ "Silver Lining"+ (Just $ Freeform "Here's your consolation prize.")+ "Lose at a scenario."+ Easy+ True
+ src/Swarm/TUI/Model/Achievement/Persistence.hs view
@@ -0,0 +1,58 @@+{-# LANGUAGE OverloadedStrings #-}++module Swarm.TUI.Model.Achievement.Persistence where++import Control.Arrow (left)+import Control.Carrier.Lift (sendIO)+import Control.Monad (forM, forM_)+import Data.Either (partitionEithers)+import Data.Yaml qualified as Y+import Swarm.TUI.Model.Achievement.Attainment+import Swarm.TUI.Model.Achievement.Definitions+import Swarm.TUI.Model.Failure+import Swarm.Util+import System.Directory (+ doesDirectoryExist,+ doesFileExist,+ listDirectory,+ )+import System.FilePath ((</>))++-- | Get path to swarm achievements, optionally creating necessary+-- directories.+getSwarmAchievementsPath :: Bool -> IO FilePath+getSwarmAchievementsPath createDirs = getSwarmXdgDataSubdir createDirs "achievement"++-- | Load saved info about achievements from XDG data directory.+-- Returns a tuple of warnings and attained achievements.+loadAchievementsInfo ::+ IO ([SystemFailure], [Attainment])+loadAchievementsInfo = do+ savedAchievementsPath <- getSwarmAchievementsPath False+ doesParentExist <- doesDirectoryExist savedAchievementsPath+ if doesParentExist+ then do+ contents <- listDirectory savedAchievementsPath+ eithersList <- forM contents $ \p -> do+ let fullPath = savedAchievementsPath </> p+ isFile <- doesFileExist fullPath+ if isFile+ then do+ eitherDecodedFile <- sendIO (Y.decodeFileEither fullPath)+ return $ left (AssetNotLoaded Achievement p . CanNotParse) eitherDecodedFile+ else return . Left $ AssetNotLoaded Achievement p (EntryNot File)+ return $ partitionEithers eithersList+ else return ([AssetNotLoaded Achievement "." (DoesNotExist Directory)], [])++-- | Save info about achievements to XDG data directory.+saveAchievementsInfo ::+ [Attainment] ->+ IO ()+saveAchievementsInfo attainmentList = do+ savedAchievementsPath <- getSwarmAchievementsPath True+ forM_ attainmentList $ \x -> do+ let achievementName = case _achievement x of+ GlobalAchievement y -> show y+ GameplayAchievement y -> show y+ fullPath = savedAchievementsPath </> achievementName+ Y.encodeFile fullPath x
+ src/Swarm/TUI/Model/Failure.hs view
@@ -0,0 +1,51 @@+{-# LANGUAGE OverloadedStrings #-}++-- |+-- Module : Swarm.Game.Failure+-- Copyright : Ondřej Šebek+-- Maintainer : ondras98@icloud.com+--+-- SPDX-License-Identifier: BSD-3-Clause+--+-- A data type to represent system failures.+--+-- These failures are often not fatal and serve+-- to create common infrastructure for logging.+module Swarm.TUI.Model.Failure where++import Data.Char (toLower)+import Data.Text (Text)+import Data.Text qualified as T+import Data.Yaml (ParseException, prettyPrintParseException)++data SystemFailure+ = AssetNotLoaded Asset FilePath LoadingFailure++data Asset = Achievement | Data | History | Save+ deriving (Eq, Show)++data Entry = Directory | File+ deriving (Eq, Show)++data LoadingFailure+ = DoesNotExist Entry+ | EntryNot Entry+ | CanNotParse ParseException++tShowLow :: Show a => a -> Text+tShowLow = T.pack . map toLower . show++tShow :: Show a => a -> Text+tShow = T.pack . show++prettyLoadingFailure :: LoadingFailure -> Text+prettyLoadingFailure = \case+ DoesNotExist e -> "The " <> tShowLow e <> " is missing!"+ EntryNot e -> "The entry is not a " <> tShowLow e <> "!"+ CanNotParse p -> "Parse failure:\n" <> T.pack (indent 8 $ prettyPrintParseException p)+ where+ indent n = unlines . map (replicate n ' ' ++) . lines++prettyFailure :: SystemFailure -> Text+prettyFailure = \case+ AssetNotLoaded a fp l -> T.unwords ["Failed to acquire", tShowLow a, tShow fp] <> ": " <> prettyLoadingFailure l
+ src/Swarm/TUI/Model/Menu.hs view
@@ -0,0 +1,129 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE ViewPatterns #-}++module Swarm.TUI.Model.Menu where++import Brick.Widgets.Dialog (Dialog)+import Brick.Widgets.List qualified as BL+import Control.Lens hiding (from, (<.>))+import Data.List.NonEmpty (NonEmpty (..))+import Data.List.NonEmpty qualified as NE+import Data.Map qualified as M+import Data.Text (Text)+import Data.Vector qualified as V+import Swarm.Game.Entity as E+import Swarm.Game.ScenarioInfo (+ ScenarioCollection,+ ScenarioInfo (..),+ ScenarioInfoPair,+ ScenarioItem (..),+ scMap,+ scenarioCollectionToList,+ )+import Swarm.Game.State+import Swarm.TUI.Model.Achievement.Definitions+import Swarm.TUI.Model.Name+import Swarm.Util+import System.FilePath (dropTrailingPathSeparator, splitPath, takeFileName)+import Witch (into)++------------------------------------------------------------+-- Menus and dialogs+------------------------------------------------------------++data ModalType+ = HelpModal+ | RecipesModal+ | CommandsModal+ | MessagesModal+ | RobotsModal+ | WinModal+ | LoseModal+ | QuitModal+ | KeepPlayingModal+ | DescriptionModal Entity+ | GoalModal+ deriving (Show)++data ButtonAction+ = Cancel+ | KeepPlaying+ | StartOver Seed ScenarioInfoPair+ | QuitAction+ | Next ScenarioInfoPair++data Modal = Modal+ { _modalType :: ModalType+ , _modalDialog :: Dialog ButtonAction Name+ }++makeLenses ''Modal++data MainMenuEntry+ = NewGame+ | Tutorial+ | Achievements+ | Messages+ | About+ | Quit+ deriving (Eq, Ord, Show, Read, Bounded, Enum)++data Menu+ = NoMenu -- We started playing directly from command line, no menu to show+ | MainMenu (BL.List Name MainMenuEntry)+ | NewGameMenu (NonEmpty (BL.List Name ScenarioItem)) -- stack of scenario item lists+ | AchievementsMenu (BL.List Name CategorizedAchievement)+ | MessagesMenu+ | AboutMenu++mainMenu :: MainMenuEntry -> BL.List Name MainMenuEntry+mainMenu e = BL.list MenuList (V.fromList listEnums) 1 & BL.listMoveToElement e++makePrisms ''Menu++-- | Create a brick 'BL.List' of scenario items from a 'ScenarioCollection'.+mkScenarioList :: Bool -> ScenarioCollection -> BL.List Name ScenarioItem+mkScenarioList cheat = flip (BL.list ScenarioList) 1 . V.fromList . filterTest . scenarioCollectionToList+ where+ filterTest = if cheat then id else filter (\case SICollection n _ -> n /= "Testing"; _ -> True)++-- | Given a 'ScenarioCollection' and a 'FilePath' which is the canonical+-- path to some folder or scenario, construct a 'NewGameMenu' stack+-- focused on the given item, if possible.+mkNewGameMenu :: Bool -> ScenarioCollection -> FilePath -> Maybe Menu+mkNewGameMenu cheat sc path = NewGameMenu . NE.fromList <$> go (Just sc) (splitPath path) []+ where+ go :: Maybe ScenarioCollection -> [FilePath] -> [BL.List Name ScenarioItem] -> Maybe [BL.List Name ScenarioItem]+ go _ [] stk = Just stk+ go Nothing _ _ = Nothing+ go (Just curSC) (thing : rest) stk = go nextSC rest (lst : stk)+ where+ hasName :: ScenarioItem -> Bool+ hasName (SISingle (_, ScenarioInfo pth _ _ _)) = takeFileName pth == thing+ hasName (SICollection nm _) = nm == into @Text (dropTrailingPathSeparator thing)++ lst = BL.listFindBy hasName (mkScenarioList cheat curSC)++ nextSC = case M.lookup (dropTrailingPathSeparator thing) (scMap curSC) of+ Just (SICollection _ c) -> Just c+ _ -> Nothing++------------------------------------------------------------+-- Inventory list entries+------------------------------------------------------------++-- | An entry in the inventory list displayed in the info panel. We+-- can either have an entity with a count in the robot's inventory,+-- an entity equipped on the robot, or a labelled separator. The+-- purpose of the separators is to show a clear distinction between+-- the robot's /inventory/ and its /equipped devices/.+data InventoryListEntry+ = Separator Text+ | InventoryEntry Count Entity+ | EquippedEntry Entity+ deriving (Eq)++makePrisms ''InventoryListEntry
+ src/Swarm/TUI/Model/Name.hs view
@@ -0,0 +1,57 @@+module Swarm.TUI.Model.Name where++data FocusablePanel+ = -- | The panel containing the REPL.+ REPLPanel+ | -- | The panel containing the world view.+ WorldPanel+ | -- | The panel showing robot info and inventory on the top left.+ RobotPanel+ | -- | The info panel on the bottom left.+ InfoPanel+ deriving (Eq, Ord, Show, Read, Bounded, Enum)++data GoalWidget+ = ObjectivesList+ | GoalSummary+ deriving (Eq, Ord, Show, Read, Bounded, Enum)++-- | Clickable buttons in modal dialogs.+data Button+ = CancelButton+ | KeepPlayingButton+ | StartOverButton+ | QuitButton+ | NextButton+ deriving (Eq, Ord, Show, Read, Bounded, Enum)++-- | 'Name' represents names to uniquely identify various components+-- of the UI, such as forms, panels, caches, extents, lists, and buttons.+data Name+ = FocusablePanel FocusablePanel+ | -- | The REPL input form.+ REPLInput+ | -- | The render cache for the world view.+ WorldCache+ | -- | The cached extent for the world view.+ WorldExtent+ | -- | The list of inventory items for the currently+ -- focused robot.+ InventoryList+ | -- | The inventory item position in the InventoryList.+ InventoryListItem Int+ | -- | The list of main menu choices.+ MenuList+ | -- | The list of achievements.+ AchievementList+ | -- | The list of goals/objectives.+ GoalWidgets GoalWidget+ | -- | The list of scenario choices.+ ScenarioList+ | -- | The scrollable viewport for the info panel.+ InfoViewport+ | -- | The scrollable viewport for any modal dialog.+ ModalViewport+ | -- | A clickable button in a modal dialog.+ Button Button+ deriving (Eq, Ord, Show, Read)
+ src/Swarm/TUI/Model/Repl.hs view
@@ -0,0 +1,266 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE ViewPatterns #-}++module Swarm.TUI.Model.Repl (+ -- ** REPL+ REPLHistItem (..),+ replItemText,+ isREPLEntry,+ getREPLEntry,+ REPLHistory,+ replIndex,+ replLength,+ replSeq,+ newREPLHistory,+ addREPLItem,+ restartREPLHistory,+ getLatestREPLHistoryItems,+ moveReplHistIndex,+ getCurrentItemText,+ replIndexIsAtInput,+ TimeDir (..),++ -- ** Prompt utils+ REPLPrompt (..),+ removeEntry,++ -- *** REPL Panel Model+ REPLState,+ ReplControlMode (..),+ replPromptType,+ replPromptEditor,+ replPromptText,+ replValid,+ replLast,+ replType,+ replControlMode,+ replHistory,+ newREPLEditor,++ -- ** Initialization+ initREPLState,+ defaultPrompt,+ lastEntry,+) where++import Brick.Widgets.Edit (Editor, applyEdit, editorText, getEditContents)+import Control.Applicative (Applicative (liftA2))+import Control.Lens hiding (from, (<.>))+import Data.Foldable (toList)+import Data.Maybe (fromMaybe, isJust)+import Data.Sequence (Seq)+import Data.Sequence qualified as Seq+import Data.Text (Text)+import Data.Text qualified as T+import Data.Text.Zipper qualified as TZ+import Swarm.Language.Types+import Swarm.TUI.Model.Name++------------------------------------------------------------+-- REPL History+------------------------------------------------------------++-- | An item in the REPL history.+data REPLHistItem+ = -- | Something entered by the user.+ REPLEntry Text+ | -- | A response printed by the system.+ REPLOutput Text+ deriving (Eq, Ord, Show, Read)++-- | Useful helper function to only get user input text.+getREPLEntry :: REPLHistItem -> Maybe Text+getREPLEntry = \case+ REPLEntry t -> Just t+ _ -> Nothing++-- | Useful helper function to filter out REPL output.+isREPLEntry :: REPLHistItem -> Bool+isREPLEntry = isJust . getREPLEntry++-- | Get the text of REPL input/output.+replItemText :: REPLHistItem -> Text+replItemText = \case+ REPLEntry t -> t+ REPLOutput t -> t++-- | History of the REPL with indices (0 is first entry) to the current+-- line and to the first entry since loading saved history.+-- We also (ab)use the length of the REPL as the index of current+-- input line, since that number is one past the index of last entry.+data REPLHistory = REPLHistory+ { _replSeq :: Seq REPLHistItem+ , _replIndex :: Int+ , _replStart :: Int+ }++makeLensesWith (lensRules & generateSignatures .~ False) ''REPLHistory++-- | Sequence of REPL inputs and outputs, oldest entry is leftmost.+replSeq :: Lens' REPLHistory (Seq REPLHistItem)++-- | The current index in the REPL history (if the user is going back+-- through the history using up/down keys).+replIndex :: Lens' REPLHistory Int++-- | The index of the first entry since loading saved history.+--+-- It will be set on load and reset on save (happens during exit).+replStart :: Lens' REPLHistory Int++-- | Create new REPL history (i.e. from loaded history file lines).+newREPLHistory :: [REPLHistItem] -> REPLHistory+newREPLHistory xs =+ let s = Seq.fromList xs+ in REPLHistory+ { _replSeq = s+ , _replStart = length s+ , _replIndex = length s+ }++-- | Point the start of REPL history after current last line. See 'replStart'.+restartREPLHistory :: REPLHistory -> REPLHistory+restartREPLHistory h = h & replStart .~ replLength h++-- | Current number lines of the REPL history - (ab)used as index of input buffer.+replLength :: REPLHistory -> Int+replLength = length . _replSeq++-- | Add new REPL input - the index must have been pointing one past+-- the last element already, so we increment it to keep it that way.+addREPLItem :: REPLHistItem -> REPLHistory -> REPLHistory+addREPLItem t h =+ h+ & replSeq %~ (|> t)+ & replIndex .~ 1 + replLength h++-- | Get the latest N items in history, starting with the oldest one.+--+-- This is used to show previous REPL lines in UI, so we need the items+-- sorted in the order they were entered and will be drawn top to bottom.+getLatestREPLHistoryItems :: Int -> REPLHistory -> [REPLHistItem]+getLatestREPLHistoryItems n h = toList latestN+ where+ latestN = Seq.drop oldestIndex $ h ^. replSeq+ oldestIndex = max (h ^. replStart) $ length (h ^. replSeq) - n++data TimeDir = Newer | Older deriving (Eq, Ord, Show)++moveReplHistIndex :: TimeDir -> Text -> REPLHistory -> REPLHistory+moveReplHistIndex d lastEntered history = history & replIndex .~ newIndex+ where+ historyLen = replLength history+ curText = fromMaybe lastEntered $ getCurrentItemText history+ curIndex = history ^. replIndex+ entries = history ^. replSeq+ -- split repl at index+ (olderP, newer) = Seq.splitAt curIndex entries+ -- find first different entry in direction+ notSameEntry = \case+ REPLEntry t -> t /= curText+ _ -> False+ newIndex = case d of+ Newer -> maybe historyLen (curIndex +) $ Seq.findIndexL notSameEntry newer+ Older -> fromMaybe curIndex $ Seq.findIndexR notSameEntry olderP++getCurrentItemText :: REPLHistory -> Maybe Text+getCurrentItemText history = replItemText <$> Seq.lookup (history ^. replIndex) (history ^. replSeq)++replIndexIsAtInput :: REPLHistory -> Bool+replIndexIsAtInput repl = repl ^. replIndex == replLength repl++-- | Given some text, removes the REPLEntry within REPLHistory which is equal to that.+-- This is used when the user enters in search mode and want to traverse the history.+-- If a command has been used many times, the history will be populated with it causing+-- the effect that search command always finds the same command.+removeEntry :: Text -> REPLHistory -> REPLHistory+removeEntry foundtext hist = hist & replSeq %~ Seq.filter (/= REPLEntry foundtext)++-- | Get the last REPLEntry in REPLHistory matching the given text+lastEntry :: Text -> REPLHistory -> Maybe Text+lastEntry t h =+ case Seq.viewr $ Seq.filter matchEntry $ h ^. replSeq of+ Seq.EmptyR -> Nothing+ _ Seq.:> a -> Just (replItemText a)+ where+ matchesText histItem = t `T.isInfixOf` replItemText histItem+ matchEntry = liftA2 (&&) matchesText isREPLEntry++------------------------------------------------------------+-- REPL+------------------------------------------------------------++-- | This data type tells us how to interpret the text typed+-- by the player at the prompt (which is stored in Editor).+data REPLPrompt+ = -- | Interpret the prompt text as a regular command.+ -- The list is for potential completions, which we can+ -- cycle through by hitting Tab repeatedly+ CmdPrompt [Text]+ | -- | Interpret the prompt text as "search this text in history"+ SearchPrompt REPLHistory++defaultPrompt :: REPLPrompt+defaultPrompt = CmdPrompt []++data ReplControlMode+ = Piloting+ | Typing+ deriving (Enum, Bounded, Eq)++data REPLState = REPLState+ { _replPromptType :: REPLPrompt+ , _replPromptEditor :: Editor Text Name+ , _replValid :: Bool+ , _replLast :: Text+ , _replType :: Maybe Polytype+ , _replControlMode :: ReplControlMode+ , _replHistory :: REPLHistory+ }++newREPLEditor :: Text -> Editor Text Name+newREPLEditor t = applyEdit gotoEnd $ editorText REPLInput (Just 1) t+ where+ ls = T.lines t+ pos = (length ls - 1, T.length (last ls))+ gotoEnd = if null ls then id else TZ.moveCursor pos++initREPLState :: REPLHistory -> REPLState+initREPLState = REPLState defaultPrompt (newREPLEditor "") True "" Nothing Typing++makeLensesWith (lensRules & generateSignatures .~ False) ''REPLState++-- | The way we interpret text typed by the player in the REPL prompt.+replPromptType :: Lens' REPLState REPLPrompt++-- | The prompt where the user can type input at the REPL.+replPromptEditor :: Lens' REPLState (Editor Text Name)++-- | Convinience lens to get text from editor and replace it with new+-- one that has the provided text.+replPromptText :: Lens' REPLState Text+replPromptText = lens g s+ where+ g r = r ^. replPromptEditor . to getEditContents . to T.concat+ s r t = r & replPromptEditor .~ newREPLEditor t++-- | Whether the prompt text is a valid 'Term'.+replValid :: Lens' REPLState Bool++-- | The type of the current REPL input which should be displayed to+-- the user (if any).+replType :: Lens' REPLState (Maybe Polytype)++-- | The last thing the user has typed which isn't part of the history.+-- This is used to restore the repl form after the user visited the history.+replLast :: Lens' REPLState Text++-- | Piloting or Typing mode+replControlMode :: Lens' REPLState ReplControlMode++-- | History of things the user has typed at the REPL, interleaved+-- with outputs the system has generated.+replHistory :: Lens' REPLState REPLHistory
+ src/Swarm/TUI/Model/StateUpdate.hs view
@@ -0,0 +1,158 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}++module Swarm.TUI.Model.StateUpdate (+ initAppState,+ startGame,+ restartGame,+ attainAchievement,+ attainAchievement',+ scenarioToAppState,+) where++import Brick.AttrMap (applyAttrMappings)+import Control.Applicative ((<|>))+import Control.Lens hiding (from, (<.>))+import Control.Monad.Except+import Control.Monad.State+import Data.List qualified as List+import Data.Map qualified as M+import Data.Maybe (fromMaybe, isJust)+import Data.Text (Text)+import Data.Time (ZonedTime, getZonedTime)+import Swarm.Game.Log (ErrorLevel (..), LogSource (ErrorTrace))+import Swarm.Game.Scenario (loadScenario, scenarioAttrs)+import Swarm.Game.Scenario.Objective.Presentation.Model (emptyGoalDisplay)+import Swarm.Game.ScenarioInfo (+ ScenarioInfo (..),+ ScenarioInfoPair,+ ScenarioStatus (..),+ normalizeScenarioPath,+ scenarioItemByPath,+ scenarioPath,+ scenarioSolution,+ scenarioStatus,+ _SISingle,+ )+import Swarm.Game.State+import Swarm.TUI.Attr (swarmAttrMap)+import Swarm.TUI.Inventory.Sorting+import Swarm.TUI.Model+import Swarm.TUI.Model.Achievement.Attainment+import Swarm.TUI.Model.Achievement.Definitions+import Swarm.TUI.Model.Achievement.Persistence+import Swarm.TUI.Model.Failure (prettyFailure)+import Swarm.TUI.Model.Repl+import Swarm.TUI.Model.UI+import Swarm.TUI.View.CustomStyling (toAttrPair)+import System.Clock++-- | Initialize the 'AppState'.+initAppState :: AppOpts -> ExceptT Text IO AppState+initAppState AppOpts {..} = do+ let isRunningInitialProgram = isJust scriptToRun || autoPlay+ skipMenu = isJust userScenario || isRunningInitialProgram || isJust userSeed+ gs <- initGameState+ (warnings, ui) <- initUIState (not skipMenu) cheatMode+ let logWarning rs w = rs & eventLog %~ logEvent (ErrorTrace Error) ("UI Loading", -8) (prettyFailure w)+ let rs = List.foldl' logWarning initRuntimeState warnings+ case skipMenu of+ False -> return $ AppState gs ui rs+ True -> do+ (scenario, path) <- loadScenario (fromMaybe "classic" userScenario) (gs ^. entityMap)++ let maybeAutoplay = do+ guard autoPlay+ soln <- scenario ^. scenarioSolution+ return $ SuggestedSolution soln+ let realToRun = maybeAutoplay <|> (ScriptPath <$> scriptToRun)++ execStateT+ (startGameWithSeed userSeed (scenario, ScenarioInfo path NotStarted NotStarted NotStarted) realToRun)+ (AppState gs ui rs)++-- | Load a 'Scenario' and start playing the game.+startGame :: (MonadIO m, MonadState AppState m) => ScenarioInfoPair -> Maybe CodeToRun -> m ()+startGame = startGameWithSeed Nothing++-- | Re-initialize the game from the stored reference to the current scenario.+--+-- Note that "restarting" is intended only for "scenarios";+-- with some scenarios, it may be possible to get stuck so that it is+-- either impossible or very annoying to win, so being offered an+-- option to restart is more user-friendly.+--+-- Since scenarios are stored as a Maybe in the UI state, we handle the Nothing+-- case upstream so that the Scenario passed to this function definitely exists.+restartGame :: (MonadIO m, MonadState AppState m) => Seed -> ScenarioInfoPair -> m ()+restartGame currentSeed siPair = startGameWithSeed (Just currentSeed) siPair Nothing++-- | Load a 'Scenario' and start playing the game, with the+-- possibility for the user to override the seed.+startGameWithSeed :: (MonadIO m, MonadState AppState m) => Maybe Seed -> ScenarioInfoPair -> Maybe CodeToRun -> m ()+startGameWithSeed userSeed siPair@(_scene, si) toRun = do+ t <- liftIO getZonedTime+ ss <- use $ gameState . scenarios+ p <- liftIO $ normalizeScenarioPath ss (si ^. scenarioPath)+ gameState . currentScenarioPath .= Just p+ gameState . scenarios . scenarioItemByPath p . _SISingle . _2 . scenarioStatus .= InProgress t 0 0+ scenarioToAppState siPair userSeed toRun++-- TODO: #516 do we need to keep an old entity map around???++-- | Modify the 'AppState' appropriately when starting a new scenario.+scenarioToAppState ::+ (MonadIO m, MonadState AppState m) =>+ ScenarioInfoPair ->+ Maybe Seed ->+ Maybe CodeToRun ->+ m ()+scenarioToAppState siPair@(scene, _) userSeed toRun = do+ withLensIO gameState $ scenarioToGameState scene userSeed toRun+ withLensIO uiState $ scenarioToUIState siPair+ where+ withLensIO :: (MonadIO m, MonadState AppState m) => Lens' AppState x -> (x -> IO x) -> m ()+ withLensIO l a = do+ x <- use l+ x' <- liftIO $ a x+ l .= x'++attainAchievement :: (MonadIO m, MonadState AppState m) => CategorizedAchievement -> m ()+attainAchievement a = do+ currentTime <- liftIO getZonedTime+ attainAchievement' currentTime Nothing a++attainAchievement' ::+ (MonadIO m, MonadState AppState m) =>+ ZonedTime ->+ Maybe FilePath ->+ CategorizedAchievement ->+ m ()+attainAchievement' t p a = do+ (uiState . uiAchievements)+ %= M.insertWith+ (<>)+ a+ (Attainment a p t)+ newAchievements <- use $ uiState . uiAchievements+ liftIO $ saveAchievementsInfo $ M.elems newAchievements++-- | Modify the UI state appropriately when starting a new scenario.+scenarioToUIState :: ScenarioInfoPair -> UIState -> IO UIState+scenarioToUIState siPair u = do+ curTime <- getTime Monotonic+ return $+ u+ & uiPlaying .~ True+ & uiGoal .~ emptyGoalDisplay+ & uiFocusRing .~ initFocusRing+ & uiInventory .~ Nothing+ & uiInventorySort .~ defaultSortOptions+ & uiShowFPS .~ False+ & uiShowZero .~ True+ & lgTicksPerSecond .~ initLgTicksPerSecond+ & uiREPL .~ initREPLState (u ^. uiREPL . replHistory)+ & uiREPL . replHistory %~ restartREPLHistory+ & uiAttrMap .~ applyAttrMappings (map toAttrPair $ fst siPair ^. scenarioAttrs) swarmAttrMap+ & scenarioRef ?~ siPair+ & lastFrameTime .~ curTime
+ src/Swarm/TUI/Model/UI.hs view
@@ -0,0 +1,312 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE ViewPatterns #-}++module Swarm.TUI.Model.UI (+ UIState (..),+ GoalDisplay (..),+ uiMenu,+ uiPlaying,+ uiCheatMode,+ uiFocusRing,+ uiWorldCursor,+ uiREPL,+ uiInventory,+ uiInventorySort,+ uiMoreInfoTop,+ uiMoreInfoBot,+ uiScrollToEnd,+ uiError,+ uiModal,+ uiGoal,+ uiAchievements,+ lgTicksPerSecond,+ lastFrameTime,+ accumulatedTime,+ tickCount,+ frameCount,+ frameTickCount,+ lastInfoTime,+ uiShowFPS,+ uiShowZero,+ uiShowRobots,+ uiHideRobotsUntil,+ uiInventoryShouldUpdate,+ uiTPF,+ uiFPS,+ uiAttrMap,+ scenarioRef,+ appData,++ -- ** Initialization+ initFocusRing,+ initLgTicksPerSecond,+ initUIState,+) where++import Brick (AttrMap)+import Brick.Focus+import Brick.Widgets.List qualified as BL+import Control.Arrow ((&&&))+import Control.Lens hiding (from, (<.>))+import Control.Monad.Except+import Data.Bits (FiniteBits (finiteBitSize))+import Data.Map (Map)+import Data.Map qualified as M+import Data.Text (Text)+import Data.Text qualified as T+import Swarm.Game.Scenario.Objective.Presentation.Model+import Swarm.Game.ScenarioInfo (+ ScenarioInfoPair,+ )+import Swarm.Game.World qualified as W+import Swarm.TUI.Attr (swarmAttrMap)+import Swarm.TUI.Inventory.Sorting+import Swarm.TUI.Model.Achievement.Attainment+import Swarm.TUI.Model.Achievement.Definitions+import Swarm.TUI.Model.Achievement.Persistence+import Swarm.TUI.Model.Failure (SystemFailure)+import Swarm.TUI.Model.Menu+import Swarm.TUI.Model.Name+import Swarm.TUI.Model.Repl+import Swarm.Util+import System.Clock++------------------------------------------------------------+-- UI state+------------------------------------------------------------++-- | The main record holding the UI state. For access to the fields,+-- see the lenses below.+data UIState = UIState+ { _uiMenu :: Menu+ , _uiPlaying :: Bool+ , _uiCheatMode :: Bool+ , _uiFocusRing :: FocusRing Name+ , _uiWorldCursor :: Maybe W.Coords+ , _uiREPL :: REPLState+ , _uiInventory :: Maybe (Int, BL.List Name InventoryListEntry)+ , _uiInventorySort :: InventorySortOptions+ , _uiMoreInfoTop :: Bool+ , _uiMoreInfoBot :: Bool+ , _uiScrollToEnd :: Bool+ , _uiError :: Maybe Text+ , _uiModal :: Maybe Modal+ , _uiGoal :: GoalDisplay+ , _uiAchievements :: Map CategorizedAchievement Attainment+ , _uiShowFPS :: Bool+ , _uiShowZero :: Bool+ , _uiHideRobotsUntil :: TimeSpec+ , _uiInventoryShouldUpdate :: Bool+ , _uiTPF :: Double+ , _uiFPS :: Double+ , _lgTicksPerSecond :: Int+ , _tickCount :: Int+ , _frameCount :: Int+ , _frameTickCount :: Int+ , _lastFrameTime :: TimeSpec+ , _accumulatedTime :: TimeSpec+ , _lastInfoTime :: TimeSpec+ , _appData :: Map Text Text+ , _uiAttrMap :: AttrMap+ , _scenarioRef :: Maybe ScenarioInfoPair+ }++--------------------------------------------------+-- Lenses for UIState++let exclude = ['_lgTicksPerSecond]+ in makeLensesWith+ ( lensRules+ & generateSignatures .~ False+ & lensField . mapped . mapped %~ \fn n ->+ if n `elem` exclude then [] else fn n+ )+ ''UIState++-- | The current menu state.+uiMenu :: Lens' UIState Menu++-- | Are we currently playing the game? True = we are playing, and+-- should thus display a world, REPL, etc.; False = we should+-- display the current menu.+uiPlaying :: Lens' UIState Bool++-- | Cheat mode, i.e. are we allowed to turn creative mode on and off?+uiCheatMode :: Lens' UIState Bool++-- | The focus ring is the set of UI panels we can cycle among using+-- the Tab key.+uiFocusRing :: Lens' UIState (FocusRing Name)++-- | The last clicked position on the world view.+uiWorldCursor :: Lens' UIState (Maybe W.Coords)++-- | The state of REPL panel.+uiREPL :: Lens' UIState REPLState++-- | The order and direction of sorting inventory list.+uiInventorySort :: Lens' UIState InventorySortOptions++-- | The hash value of the focused robot entity (so we can tell if its+-- inventory changed) along with a list of the items in the+-- focused robot's inventory.+uiInventory :: Lens' UIState (Maybe (Int, BL.List Name InventoryListEntry))++-- | Does the info panel contain more content past the top of the panel?+uiMoreInfoTop :: Lens' UIState Bool++-- | Does the info panel contain more content past the bottom of the panel?+uiMoreInfoBot :: Lens' UIState Bool++-- | A flag telling the UI to scroll the info panel to the very end+-- (used when a new log message is appended).+uiScrollToEnd :: Lens' UIState Bool++-- | When this is @Just@, it represents a popup box containing an+-- error message that is shown on top of the rest of the UI.+uiError :: Lens' UIState (Maybe Text)++-- | When this is @Just@, it represents a modal to be displayed on+-- top of the UI, e.g. for the Help screen.+uiModal :: Lens' UIState (Maybe Modal)++-- | Status of the scenario goal: whether there is one, and whether it+-- has been displayed to the user initially.+uiGoal :: Lens' UIState GoalDisplay++-- | Map of achievements that were attained+uiAchievements :: Lens' UIState (Map CategorizedAchievement Attainment)++-- | A toggle to show the FPS by pressing `f`+uiShowFPS :: Lens' UIState Bool++-- | A toggle to show or hide inventory items with count 0 by pressing `0`+uiShowZero :: Lens' UIState Bool++-- | Hide robots on the world map.+uiHideRobotsUntil :: Lens' UIState TimeSpec++-- | Whether to show or hide robots on the world map.+uiShowRobots :: Getter UIState Bool+uiShowRobots = to (\ui -> ui ^. lastFrameTime > ui ^. uiHideRobotsUntil)++-- | Whether the Inventory ui panel should update+uiInventoryShouldUpdate :: Lens' UIState Bool++-- | Computed ticks per milli seconds+uiTPF :: Lens' UIState Double++-- | Computed frames per milli seconds+uiFPS :: Lens' UIState Double++-- | Attribute map+uiAttrMap :: Lens' UIState AttrMap++-- | The currently active Scenario description, useful for starting over.+scenarioRef :: Lens' UIState (Maybe ScenarioInfoPair)++-- | The base-2 logarithm of the current game speed in ticks/second.+-- Note that we cap this value to the range of +/- log2 INTMAX.+lgTicksPerSecond :: Lens' UIState Int+lgTicksPerSecond = lens _lgTicksPerSecond safeSetLgTicks+ where+ maxLog = finiteBitSize (maxBound :: Int)+ maxTicks = maxLog - 2+ minTicks = 2 - maxLog+ safeSetLgTicks ui lTicks+ | lTicks < minTicks = setLgTicks ui minTicks+ | lTicks > maxTicks = setLgTicks ui maxTicks+ | otherwise = setLgTicks ui lTicks+ setLgTicks ui lTicks = ui {_lgTicksPerSecond = lTicks}++-- | A counter used to track how many ticks have happened since the+-- last time we updated the ticks/frame statistics.+tickCount :: Lens' UIState Int++-- | A counter used to track how many frames have been rendered since the+-- last time we updated the ticks/frame statistics.+frameCount :: Lens' UIState Int++-- | A counter used to track how many ticks have happened in the+-- current frame, so we can stop when we get to the tick cap.+frameTickCount :: Lens' UIState Int++-- | The time of the last info widget update+lastInfoTime :: Lens' UIState TimeSpec++-- | The time of the last 'Frame' event.+lastFrameTime :: Lens' UIState TimeSpec++-- | The amount of accumulated real time. Every time we get a 'Frame'+-- event, we accumulate the amount of real time that happened since+-- the last frame, then attempt to take an appropriate number of+-- ticks to "catch up", based on the target tick rate.+--+-- See https://gafferongames.com/post/fix_your_timestep/ .+accumulatedTime :: Lens' UIState TimeSpec++-- | Free-form data loaded from the @data@ directory, for things like+-- the logo, about page, tutorial story, etc.+appData :: Lens' UIState (Map Text Text)++--------------------------------------------------+-- UIState initialization++-- | The initial state of the focus ring.+initFocusRing :: FocusRing Name+initFocusRing = focusRing $ map FocusablePanel listEnums++-- | The initial tick speed.+initLgTicksPerSecond :: Int+initLgTicksPerSecond = 4 -- 2^4 = 16 ticks / second++-- | Initialize the UI state. This needs to be in the IO monad since+-- it involves reading a REPL history file, getting the current+-- time, and loading text files from the data directory. The @Bool@+-- parameter indicates whether we should start off by showing the+-- main menu.+initUIState :: Bool -> Bool -> ExceptT Text IO ([SystemFailure], UIState)+initUIState showMainMenu cheatMode = do+ historyT <- liftIO $ readFileMayT =<< getSwarmHistoryPath False+ appDataMap <- liftIO readAppData+ let history = maybe [] (map REPLEntry . T.lines) historyT+ startTime <- liftIO $ getTime Monotonic+ (warnings, achievements) <- liftIO loadAchievementsInfo+ let out =+ UIState+ { _uiMenu = if showMainMenu then MainMenu (mainMenu NewGame) else NoMenu+ , _uiPlaying = not showMainMenu+ , _uiCheatMode = cheatMode+ , _uiFocusRing = initFocusRing+ , _uiWorldCursor = Nothing+ , _uiREPL = initREPLState $ newREPLHistory history+ , _uiInventory = Nothing+ , _uiInventorySort = defaultSortOptions+ , _uiMoreInfoTop = False+ , _uiMoreInfoBot = False+ , _uiScrollToEnd = False+ , _uiError = Nothing+ , _uiModal = Nothing+ , _uiGoal = emptyGoalDisplay+ , _uiAchievements = M.fromList $ map (view achievement &&& id) achievements+ , _uiShowFPS = False+ , _uiShowZero = True+ , _uiHideRobotsUntil = startTime - 1+ , _uiInventoryShouldUpdate = False+ , _uiTPF = 0+ , _uiFPS = 0+ , _lgTicksPerSecond = initLgTicksPerSecond+ , _lastFrameTime = startTime+ , _accumulatedTime = 0+ , _lastInfoTime = 0+ , _tickCount = 0+ , _frameCount = 0+ , _frameTickCount = 0+ , _appData = appDataMap+ , _uiAttrMap = swarmAttrMap+ , _scenarioRef = Nothing+ }+ return (warnings, out)
src/Swarm/TUI/View.hs view
@@ -38,7 +38,7 @@ drawREPL, ) where -import Brick hiding (Direction)+import Brick hiding (Direction, Location) import Brick.Focus import Brick.Forms import Brick.Widgets.Border (hBorder, hBorderWithLabel, joinableBorder, vBorder)@@ -49,7 +49,6 @@ import Brick.Widgets.Table qualified as BT import Control.Lens hiding (Const, from) import Control.Monad (guard)-import Control.Monad.Reader (withReaderT) import Data.Array (range) import Data.Bits (shiftL, shiftR, (.&.)) import Data.Foldable qualified as F@@ -68,8 +67,8 @@ import Data.Text (Text) import Data.Text qualified as T import Data.Time (NominalDiffTime, defaultTimeLocale, formatTime)-import Graphics.Vty qualified as V import Linear+import Linear.Affine (Point) import Network.Wai.Handler.Warp (Port) import Swarm.Game.CESK (CESK (..)) import Swarm.Game.Display@@ -77,6 +76,8 @@ import Swarm.Game.Recipe import Swarm.Game.Robot import Swarm.Game.Scenario (scenarioAuthor, scenarioDescription, scenarioName, scenarioObjectives)+import Swarm.Game.Scenario.Objective.Presentation.Model (goalsContent, hasAnythingToShow)+import Swarm.Game.Scenario.Objective.Presentation.Render qualified as GR import Swarm.Game.ScenarioInfo ( ScenarioItem (..), ScenarioStatus (..),@@ -86,18 +87,23 @@ scenarioStatus, ) import Swarm.Game.State-import Swarm.Game.Terrain (terrainMap) import Swarm.Game.World qualified as W+import Swarm.Language.Capability (constCaps) import Swarm.Language.Pretty (prettyText) import Swarm.Language.Syntax import Swarm.Language.Typecheck (inferConst)-import Swarm.Language.Types (Polytype) import Swarm.TUI.Attr import Swarm.TUI.Border import Swarm.TUI.Inventory.Sorting (renderSortMethod) import Swarm.TUI.Model+import Swarm.TUI.Model.Repl+import Swarm.TUI.Model.UI import Swarm.TUI.Panel+import Swarm.TUI.View.Achievement+import Swarm.TUI.View.CellDisplay+import Swarm.TUI.View.Util import Swarm.Util+import Swarm.Util.Location import Swarm.Version (NewReleaseFailure (..)) import System.Clock (TimeSpec (..)) import Text.Printf@@ -110,19 +116,20 @@ drawUI s | s ^. uiState . uiPlaying = drawGameUI s | otherwise = case s ^. uiState . uiMenu of- -- We should never reach the NoMenu case if uiPlaying is false; we would have- -- quit the app instead. But just in case, we display the main menu anyway.- NoMenu -> [drawMainMenuUI s (mainMenu NewGame)]- MainMenu l -> [drawMainMenuUI s l]- NewGameMenu stk -> [drawNewGameMenuUI stk]- MessagesMenu -> [drawMainMessages s]- AboutMenu -> [drawAboutMenuUI (s ^. uiState . appData . at "about")]+ -- We should never reach the NoMenu case if uiPlaying is false; we would have+ -- quit the app instead. But just in case, we display the main menu anyway.+ NoMenu -> [drawMainMenuUI s (mainMenu NewGame)]+ MainMenu l -> [drawMainMenuUI s l]+ NewGameMenu stk -> [drawNewGameMenuUI stk]+ AchievementsMenu l -> [drawAchievementsMenuUI s l]+ MessagesMenu -> [drawMainMessages s]+ AboutMenu -> [drawAboutMenuUI (s ^. uiState . appData . at "about")] drawMainMessages :: AppState -> Widget Name drawMainMessages s = renderDialog dial . padBottom Max . scrollList $ drawLogs ls where ls = reverse $ s ^. runtimeState . eventLog . notificationsContent- dial = dialog (Just "Messages") Nothing maxModalWindowWidth+ dial = dialog (Just $ str "Messages") Nothing maxModalWindowWidth scrollList = withVScrollBars OnRight . vBox drawLogs = map (drawLogEntry True) @@ -131,7 +138,7 @@ vBox . catMaybes $ [ drawLogo <$> logo , hCenter . padTopBottom 2 <$> newVersionWidget version- , Just . centerLayer . vLimit 5 . hLimit 20 $+ , Just . centerLayer . vLimit 6 . hLimit 20 $ BL.renderList (const (hCenter . drawMainMenuEntry s)) True l ] where@@ -169,7 +176,8 @@ [ vBox [ withAttr boldAttr . txt $ breadcrumbs ls , txt " "- , vLimit 20 . hLimit 35+ , vLimit 20+ . hLimit 35 . BL.renderList (const $ padRight Max . drawScenarioItem) True $ l ]@@ -244,6 +252,7 @@ drawMainMenuEntry s = \case NewGame -> txt "New game" Tutorial -> txt "Tutorial"+ Achievements -> txt "Achievements" About -> txt "About" Messages -> highlightMessages $ txt "Messages" Quit -> txt "Quit"@@ -271,16 +280,16 @@ hBox [ hLimitPercent 25 $ vBox- [ vLimitPercent 50 $ panel highlightAttr fr RobotPanel plainBorder $ drawRobotPanel s+ [ vLimitPercent 50 $ panel highlightAttr fr (FocusablePanel RobotPanel) plainBorder $ drawRobotPanel s , panel highlightAttr fr- InfoPanel+ (FocusablePanel InfoPanel) ( plainBorder & topLabels . centerLabel- .~ (if moreTop then Just (txt " · · · ") else Nothing)+ .~ (if moreTop then Just (txt " · · · ") else Nothing) & bottomLabels . centerLabel- .~ (if moreBot then Just (txt " · · · ") else Nothing)+ .~ (if moreBot then Just (txt " · · · ") else Nothing) ) $ drawInfoPanel s ]@@ -288,7 +297,7 @@ [ panel highlightAttr fr- WorldPanel+ (FocusablePanel WorldPanel) ( plainBorder & bottomLabels . rightLabel ?~ padLeftRight 1 (drawTPS s) & topLabels . leftLabel ?~ drawModalMenu s@@ -297,11 +306,11 @@ ) (drawWorld (s ^. uiState . uiShowRobots) (s ^. gameState)) , drawKeyMenu s- , clickable REPLPanel $+ , clickable (FocusablePanel REPLPanel) $ panel highlightAttr fr- REPLPanel+ (FocusablePanel REPLPanel) ( plainBorder & topLabels . rightLabel .~ (drawType <$> (s ^. uiState . uiREPL . replType)) )@@ -317,19 +326,41 @@ addCursorPos = case s ^. uiState . uiWorldCursor of Nothing -> id Just coord ->- let worlCursorInfo = drawWorldCursorInfo (s ^. uiState . uiShowRobots) (s ^. gameState) coord- in bottomLabels . leftLabel ?~ padLeftRight 1 worlCursorInfo+ let worldCursorInfo = drawWorldCursorInfo (s ^. gameState) coord+ in bottomLabels . leftLabel ?~ padLeftRight 1 worldCursorInfo -- Add clock display in top right of the world view if focused robot- -- has a clock installed+ -- has a clock equipped addClock = topLabels . rightLabel ?~ padLeftRight 1 (drawClockDisplay $ s ^. gameState) fr = s ^. uiState . uiFocusRing moreTop = s ^. uiState . uiMoreInfoTop moreBot = s ^. uiState . uiMoreInfoBot -drawWorldCursorInfo :: Bool -> GameState -> W.Coords -> Widget Name-drawWorldCursorInfo showRobots g i@(W.Coords (y, x)) =- hBox [drawLoc showRobots g i, txt $ " at " <> from (show x) <> " " <> from (show (y * (-1)))]+drawWorldCursorInfo :: GameState -> W.Coords -> Widget Name+drawWorldCursorInfo g coords@(W.Coords (y, x)) =+ hBox $ tileMemberWidgets ++ [coordsWidget]+ where+ coordsWidget =+ txt $+ T.unwords+ [ from $ show x+ , from $ show $ y * (-1)+ ] + tileMembers = terrain : mapMaybe merge [entity, robot]+ tileMemberWidgets =+ map (padRight $ Pad 1) $+ concat $+ reverse $+ zipWith f tileMembers ["at", "on", "with"]+ where+ f cell preposition = [renderDisplay cell, txt preposition]++ terrain = displayTerrainCell g coords+ entity = displayEntityCell g coords+ robot = displayRobotCell g coords++ merge = fmap sconcat . NE.nonEmpty . filter (not . (^. invisible))+ -- | Format the clock display to be shown in the upper right of the -- world panel. drawClockDisplay :: GameState -> Widget n@@ -339,12 +370,12 @@ pauseWidget = guard (gs ^. paused) $> txt "(PAUSED)" -- | Check whether the currently focused robot (if any) has a clock--- device installed.-clockInstalled :: GameState -> Bool-clockInstalled gs = case focusedRobot gs of+-- device equipped.+clockEquipped :: GameState -> Bool+clockEquipped gs = case focusedRobot gs of Nothing -> False Just r- | countByName "clock" (r ^. installedDevices) > 0 -> True+ | countByName "clock" (r ^. equippedDevices) > 0 -> True | otherwise -> False -- | Format a ticks count as a hexadecimal clock.@@ -360,16 +391,12 @@ ++ if showTicks then [".", printf "%x" (t .&. ((1 `shiftL` 4) - 1))] else [] -- | Return a possible time display, if the currently focused robot--- has a clock device installed. The first argument is the number+-- has a clock device equipped. The first argument is the number -- of ticks (e.g. 943 = 0x3af), and the second argument indicates -- whether the time should be shown down to single-tick resolution -- (e.g. 0:00:3a.f) or not (e.g. 0:00:3a). maybeDrawTime :: Integer -> Bool -> GameState -> Maybe (Widget n)-maybeDrawTime t showTicks gs = guard (clockInstalled gs) $> drawTime t showTicks---- | Render the type of the current REPL input to be shown to the user.-drawType :: Polytype -> Widget Name-drawType = withAttr infoAttr . padLeftRight 1 . txt . prettyText+maybeDrawTime t showTicks gs = guard (clockEquipped gs) $> drawTime t showTicks -- | Draw info about the current number of ticks per second. drawTPS :: AppState -> Widget Name@@ -381,12 +408,12 @@ rateInfo | s ^. uiState . uiShowFPS =- [ txt " ("- , str (printf "%0.1f" (s ^. uiState . uiTPF))- , txt " tpf, "- , str (printf "%0.1f" (s ^. uiState . uiFPS))- , txt " fps)"- ]+ [ txt " ("+ , str (printf "%0.1f" (s ^. uiState . uiTPF))+ , txt " tpf, "+ , str (printf "%0.1f" (s ^. uiState . uiFPS))+ , txt " fps)"+ ] | otherwise = [] l = s ^. uiState . lgTicksPerSecond@@ -403,13 +430,9 @@ Nothing -> showFirstCursor s locs Just _ -> Nothing --- | Width cap for modal and error message windows-maxModalWindowWidth :: Int-maxModalWindowWidth = 500- -- | Render the error dialog window with a given error message renderErrorDialog :: Text -> Widget Name-renderErrorDialog err = renderDialog (dialog (Just "Error") Nothing (maxModalWindowWidth `min` requiredWidth)) errContent+renderErrorDialog err = renderDialog (dialog (Just $ str "Error") Nothing (maxModalWindowWidth `min` requiredWidth)) errContent where errContent = txtWrapWith indent2 {preserveIndentation = True} err requiredWidth = 2 + maximum (textWidth <$> T.lines err)@@ -417,110 +440,33 @@ -- | Draw the error dialog window, if it should be displayed right now. drawDialog :: AppState -> Widget Name drawDialog s = case s ^. uiState . uiModal of- Just (Modal mt d) -> renderDialog d (maybeScroll ModalViewport $ drawModal s mt)+ Just (Modal mt d) -> renderDialog d $ case mt of+ GoalModal -> drawModal s mt+ _ -> maybeScroll ModalViewport $ drawModal s mt Nothing -> maybe emptyWidget renderErrorDialog (s ^. uiState . uiError) --- | Make a widget scrolling if it is bigger than the available--- vertical space. Thanks to jtdaugherty for this code.-maybeScroll :: (Ord n, Show n) => n -> Widget n -> Widget n-maybeScroll vpName contents =- Widget Greedy Greedy $ do- ctx <- getContext- result <- withReaderT (availHeightL .~ 10000) (render contents)- if V.imageHeight (result ^. imageL) <= ctx ^. availHeightL- then return result- else- render $- withVScrollBars OnRight $- viewport vpName Vertical $- Widget Fixed Fixed $- return result- -- | Draw one of the various types of modal dialog. drawModal :: AppState -> ModalType -> Widget Name drawModal s = \case HelpModal -> helpWidget (s ^. gameState . seed) (s ^. runtimeState . webPort) RobotsModal -> robotsListWidget s RecipesModal -> availableListWidget (s ^. gameState) RecipeList- CommandsModal -> availableListWidget (s ^. gameState) CommandList+ CommandsModal -> commandsListWidget (s ^. gameState) MessagesModal -> availableListWidget (s ^. gameState) MessageList WinModal -> padBottom (Pad 1) $ hCenter $ txt "Congratulations!"+ LoseModal ->+ padBottom (Pad 1) $+ vBox $+ map+ (hCenter . txt)+ [ "Condolences!"+ , "This scenario is no longer winnable."+ ] DescriptionModal e -> descriptionWidget s e QuitModal -> padBottom (Pad 1) $ hCenter $ txt (quitMsg (s ^. uiState . uiMenu))- GoalModal g -> padLeftRight 1 (displayParagraphs g)+ GoalModal -> GR.renderGoalsDisplay (s ^. uiState . uiGoal) KeepPlayingModal -> padLeftRight 1 (displayParagraphs ["Have fun! Hit Ctrl-Q whenever you're ready to proceed to the next challenge or return to the menu."]) -quitMsg :: Menu -> Text-quitMsg m = "Are you sure you want to " <> quitAction <> "? All progress on this scenario will be lost!"- where- quitAction = case m of- NoMenu -> "quit"- _ -> "return to the menu"---- | Generate a fresh modal window of the requested type.-generateModal :: AppState -> ModalType -> Modal-generateModal s mt = Modal mt (dialog (Just title) buttons (maxModalWindowWidth `min` requiredWidth))- where- currentScenario = s ^. uiState . scenarioRef- currentSeed = s ^. gameState . seed- haltingMessage = case s ^. uiState . uiMenu of- NoMenu -> Just "Quit"- _ -> Nothing- descriptionWidth = 100- helpWidth = 80- (title, buttons, requiredWidth) =- case mt of- HelpModal -> (" Help ", Nothing, helpWidth)- RobotsModal -> ("Robots", Nothing, descriptionWidth)- RecipesModal -> ("Available Recipes", Nothing, descriptionWidth)- CommandsModal -> ("Available Commands", Nothing, descriptionWidth)- MessagesModal -> ("Messages", Nothing, descriptionWidth)- WinModal ->- let nextMsg = "Next challenge!"- stopMsg = fromMaybe "Return to the menu" haltingMessage- continueMsg = "Keep playing"- in ( ""- , Just- ( 0- , [ (nextMsg, NextButton scene)- | Just scene <- [nextScenario (s ^. uiState . uiMenu)]- ]- ++ [ (stopMsg, QuitButton)- , (continueMsg, KeepPlayingButton)- ]- )- , sum (map length [nextMsg, stopMsg, continueMsg]) + 32- )- DescriptionModal e -> (descriptionTitle e, Nothing, descriptionWidth)- QuitModal ->- let stopMsg = fromMaybe ("Quit to" ++ maybe "" (" " ++) (into @String <$> curMenuName s) ++ " menu") haltingMessage- maybeStartOver = sequenceA ("Start over", StartOverButton currentSeed <$> currentScenario)- in ( ""- , Just- ( 0- , catMaybes- [ Just ("Keep playing", CancelButton)- , maybeStartOver- , Just (stopMsg, QuitButton)- ]- )- , T.length (quitMsg (s ^. uiState . uiMenu)) + 4- )- GoalModal _ ->- let goalModalTitle = case currentScenario of- Nothing -> "Goal"- Just (scenario, _) -> scenario ^. scenarioName- in (" " <> T.unpack goalModalTitle <> " ", Nothing, 80)- KeepPlayingModal -> ("", Just (0, [("OK", CancelButton)]), 80)---- | Get the name of the current New Game menu.-curMenuName :: AppState -> Maybe Text-curMenuName s = case s ^. uiState . uiMenu of- NewGameMenu (_ :| (parentMenu : _)) ->- Just (parentMenu ^. BL.listSelectedElementL . to scenarioItemName)- NewGameMenu _ -> Just "Scenarios"- _ -> Nothing- robotsListWidget :: AppState -> Widget Name robotsListWidget s = hCenter table where@@ -571,7 +517,7 @@ locWidget = hBox [worldCell, txt $ " " <> locStr] where- rloc@(V2 x y) = robot ^. robotLocation+ rloc@(Location x y) = robot ^. robotLocation worldCell = drawLoc (s ^. uiState . uiShowRobots) g (W.locToCoords rloc) locStr = from (show x) <> " " <> from (show y) @@ -581,9 +527,9 @@ | isActive robot -> withAttr notifAttr $ txt "busy" | otherwise -> withAttr greenAttr $ txt "idle" - basePos :: V2 Double- basePos = realToFrac <$> fromMaybe (V2 0 0) (g ^? baseRobot . robotLocation)- -- Keep the base and non sytem robot (e.g. no seed)+ basePos :: Point V2 Double+ basePos = realToFrac <$> fromMaybe origin (g ^? baseRobot . robotLocation)+ -- Keep the base and non system robot (e.g. no seed) isRelevant robot = robot ^. robotID == 0 || not (robot ^. systemRobot) -- Keep the robot that are less than 32 unit away from the base isNear robot = creative || distance (realToFrac <$> robot ^. robotLocation) basePos < 32@@ -649,18 +595,16 @@ , ("Meta-t", "focus on the info panel") ] -data NotificationList = RecipeList | CommandList | MessageList+data NotificationList = RecipeList | MessageList availableListWidget :: GameState -> NotificationList -> Widget Name availableListWidget gs nl = padTop (Pad 1) $ vBox widgetList where widgetList = case nl of RecipeList -> mkAvailableList gs availableRecipes renderRecipe- CommandList -> mkAvailableList gs availableCommands renderCommand & (<> constWiki) . (padLeftRight 18 constHeader :) MessageList -> messagesWidget gs renderRecipe = padLeftRight 18 . drawRecipe Nothing (fromMaybe E.empty inv) inv = gs ^? to focusedRobot . _Just . robotInventory- renderCommand = padLeftRight 18 . drawConst mkAvailableList :: GameState -> Lens' GameState (Notifications a) -> (a -> Widget Name) -> [Widget Name] mkAvailableList gs notifLens notifRender = map padRender news <> notifSep <> map padRender knowns@@ -670,28 +614,59 @@ (news, knowns) = splitAt count (gs ^. notifLens . notificationsContent) notifSep | count > 0 && not (null knowns) =- [ padBottom (Pad 1) (withAttr redAttr $ hBorderWithLabel (padLeftRight 1 (txt "new↑")))- ]+ [ padBottom (Pad 1) (withAttr redAttr $ hBorderWithLabel (padLeftRight 1 (txt "new↑")))+ ] | otherwise = [] -constHeader :: Widget Name-constHeader = padBottom (Pad 1) $ withAttr robotAttr $ padLeft (Pad 1) $ txt "command name : type"+commandsListWidget :: GameState -> Widget Name+commandsListWidget gs =+ hCenter $+ vBox+ [ table+ , padTop (Pad 1) $ txt "For the full list of available commands see the Wiki at:"+ , txt "https://github.com/swarm-game/swarm/wiki/Commands-Cheat-Sheet"+ ]+ where+ commands = gs ^. availableCommands . notificationsContent+ table =+ BT.renderTable+ . BT.surroundingBorder False+ . BT.columnBorders False+ . BT.rowBorders False+ . BT.setDefaultColAlignment BT.AlignLeft+ . BT.alignRight 0+ . BT.table+ $ headers : commandsTable+ headers =+ withAttr robotAttr+ <$> [ txt "command name"+ , txt " : type"+ , txt "Enabled by"+ ] -constWiki :: [Widget Name]-constWiki =- padLeftRight 13- <$> [ padTop (Pad 2) $ txt "For the full list of available commands see the Wiki at:"- , txt "https://github.com/swarm-game/swarm/wiki/Commands-Cheat-Sheet"- ]+ commandsTable = mkCmdRow <$> commands+ mkCmdRow cmd =+ map+ (padTop $ Pad 1)+ [ txt $ syntax $ constInfo cmd+ , padRight (Pad 2) $ txt $ " : " <> prettyText (inferConst cmd)+ , listDevices cmd+ ] -drawConst :: Const -> Widget Name-drawConst c = hBox [padLeft (Pad $ 13 - T.length constName) (txt constName), txt constSig]- where- constName = syntax . constInfo $ c- constSig = " : " <> prettyText (inferConst c)+ base = gs ^? baseRobot+ entsByCap = case base of+ Just r ->+ M.map NE.toList $+ entitiesByCapability $+ (r ^. equippedDevices) `union` (r ^. robotInventory)+ Nothing -> mempty -descriptionTitle :: Entity -> String-descriptionTitle e = " " ++ from @Text (e ^. entityName) ++ " "+ listDevices cmd = vBox $ map drawLabelledEntityName providerDevices+ where+ providerDevices =+ concatMap (flip (M.findWithDefault []) entsByCap) $+ maybeToList $+ constCaps cmd -- | Generate a pop-up widget to display the description of an entity. descriptionWidget :: AppState -> Entity -> Widget Name@@ -713,10 +688,14 @@ txt2 = txtWrapWith indent2 colorLogs :: LogEntry -> AttrName-colorLogs e = case e ^. leSaid of+colorLogs e = case e ^. leSource of Said -> robotColor (e ^. leRobotID) Logged -> notifAttr- ErrorTrace -> redAttr+ ErrorTrace l -> case l of+ Debug -> dimAttr+ Warning -> yellowAttr+ Error -> redAttr+ Critical -> redAttr where -- color each robot message with different color of the world robotColor rid = fgCols !! (rid `mod` fgColLen)@@ -731,10 +710,10 @@ notificationKey notifLens key name | null (s ^. gameState . notifLens . notificationsContent) = Nothing | otherwise =- let highlight- | s ^. gameState . notifLens . notificationsCount > 0 = Alert- | otherwise = NoHighlight- in Just (highlight, key, name)+ let highlight+ | s ^. gameState . notifLens . notificationsCount > 0 = Alert+ | otherwise = NoHighlight+ in Just (highlight, key, name) globalKeyCmds = catMaybes@@ -752,30 +731,41 @@ -- This excludes the F-key modals that are shown elsewhere. drawKeyMenu :: AppState -> Widget Name drawKeyMenu s =- vLimit 1- . hBox- . (++ [gameModeWidget])- . map (padLeftRight 1 . drawKeyCmd)- . (globalKeyCmds ++)- . map highlightKeyCmds- . keyCmdsFor- . focusGetCurrent- . view (uiState . uiFocusRing)- $ s+ vLimit 2 $+ hBox+ [ vBox+ [ mkCmdRow globalKeyCmds+ , padLeft (Pad 2) $ mkCmdRow focusedPanelCmds+ ]+ , gameModeWidget+ ] where+ mkCmdRow = hBox . map drawPaddedCmd+ drawPaddedCmd = padLeftRight 1 . drawKeyCmd+ focusedPanelCmds =+ map highlightKeyCmds $+ keyCmdsFor $+ focusGetCurrent $+ view (uiState . uiFocusRing) s+ isReplWorking = s ^. gameState . replWorking isPaused = s ^. gameState . paused viewingBase = (s ^. gameState . viewCenterRule) == VCRobot 0 creative = s ^. gameState . creativeMode cheat = s ^. uiState . uiCheatMode- goal = case s ^. uiState . uiGoal of- Just g | g /= [] -> True- _ -> False+ goal = hasAnythingToShow $ s ^. uiState . uiGoal . goalsContent showZero = s ^. uiState . uiShowZero inventorySort = s ^. uiState . uiInventorySort+ ctrlMode = s ^. uiState . uiREPL . replControlMode + renderControlModeSwitch :: ReplControlMode -> T.Text+ renderControlModeSwitch = \case+ Piloting -> "REPL"+ Typing -> "pilot"+ gameModeWidget =- padLeft Max . padLeftRight 1+ padLeft Max+ . padLeftRight 1 . txt . (<> " mode") $ case creative of@@ -796,23 +786,24 @@ "pop out" | (s ^. uiState . uiMoreInfoBot) || (s ^. uiState . uiMoreInfoTop) -> Alert _ -> PanelSpecific - keyCmdsFor (Just REPLPanel) =+ keyCmdsFor (Just (FocusablePanel REPLPanel)) = [ ("↓↑", "history") ] ++ [("Enter", "execute") | not isReplWorking] ++ [("^c", "cancel") | isReplWorking]- keyCmdsFor (Just WorldPanel) =+ ++ [("M-p", renderControlModeSwitch ctrlMode) | creative]+ keyCmdsFor (Just (FocusablePanel WorldPanel)) = [ ("←↓↑→ / hjkl", "scroll") | creative ] ++ [("c", "recenter") | not viewingBase] ++ [("f", "FPS")]- keyCmdsFor (Just RobotPanel) =+ keyCmdsFor (Just (FocusablePanel RobotPanel)) = [ ("Enter", "pop out") , ("m", "make") , ("0", (if showZero then "hide" else "show") <> " 0") , (":/;", T.unwords ["Sort:", renderSortMethod inventorySort]) ]- keyCmdsFor (Just InfoPanel) = []+ keyCmdsFor (Just (FocusablePanel InfoPanel)) = [] keyCmdsFor _ = [] data KeyHighlight = NoHighlight | Alert | PanelSpecific@@ -841,7 +832,7 @@ . cached WorldCache . reportExtent WorldExtent -- Set the clickable request after the extent to play nice with the cache- . clickable WorldPanel+ . clickable (FocusablePanel WorldPanel) . Widget Fixed Fixed $ do ctx <- getContext@@ -850,41 +841,6 @@ ixs = range (viewingRegion g (fromIntegral w, fromIntegral h)) render . vBox . map hBox . chunksOf w . map (drawLoc showRobots g) $ ixs --- | Render the 'Display' for a specific location.-drawLoc :: Bool -> GameState -> W.Coords -> Widget Name-drawLoc showRobots g = renderDisplay . displayLoc showRobots g---- | Get the 'Display' for a specific location, by combining the--- 'Display's for the terrain, entity, and robots at the location.-displayLoc :: Bool -> GameState -> W.Coords -> Display-displayLoc showRobots g coords =- sconcat $ terrain NE.:| entity <> robots- where- terrain = terrainMap M.! toEnum (W.lookupTerrain coords (g ^. world))- entity = maybeToList (displayForEntity <$> W.lookupEntity coords (g ^. world))- robots =- if showRobots- then map (view robotDisplay) (robotsAtLocation (W.coordsToLoc coords) g)- else []-- displayForEntity :: Entity -> Display- displayForEntity e = (if known e then id else hidden) (e ^. entityDisplay)-- known e =- e `hasProperty` Known- || (e ^. entityName) `elem` (g ^. knownEntities)- || case hidingMode g of- HideAllEntities -> False- HideNoEntity -> True- HideEntityUnknownTo ro -> ro `robotKnows` e--data HideEntity = HideAllEntities | HideNoEntity | HideEntityUnknownTo Robot--hidingMode :: GameState -> HideEntity-hidingMode g- | g ^. creativeMode = HideNoEntity- | otherwise = maybe HideAllEntities HideEntityUnknownTo $ focusedRobot g- ------------------------------------------------------------ -- Robot inventory panel ------------------------------------------------------------@@ -894,7 +850,7 @@ drawRobotPanel :: AppState -> Widget Name drawRobotPanel s = case (s ^. gameState . to focusedRobot, s ^. uiState . uiInventory) of (Just r, Just (_, lst)) ->- let V2 x y = r ^. robotLocation+ let Location x y = r ^. robotLocation drawClickableItem pos selb = clickable (InventoryListItem pos) . drawItem (lst ^. BL.listSelectedL) pos selb in padBottom Max $ vBox@@ -931,7 +887,7 @@ drawItem _ _ _ (InventoryEntry n e) = drawLabelledEntityName e <+> showCount n where showCount = padLeft Max . str . show-drawItem _ _ _ (InstalledEntry e) = drawLabelledEntityName e <+> padLeft Max (str " ")+drawItem _ _ _ (EquippedEntry e) = drawLabelledEntityName e <+> padLeft Max (str " ") -- | Draw the name of an entity, labelled with its visual -- representation as a cell in the world.@@ -959,9 +915,9 @@ explainFocusedItem :: AppState -> Widget Name explainFocusedItem s = case focusedItem s of Just (InventoryEntry _ e) -> explainEntry s e- Just (InstalledEntry e) ->+ Just (EquippedEntry e) -> explainEntry s e- -- Special case: installed logger device displays the robot's log.+ -- Special case: equipped logger device displays the robot's log. <=> if e ^. entityName == "logger" then drawRobotLog s else emptyWidget _ -> txt " " @@ -998,13 +954,13 @@ explainRecipes s e | null recipes = emptyWidget | otherwise =- vBox- [ padBottom (Pad 1) (hBorderWithLabel (txt "Recipes"))- , padLeftRight 2 $- hCenter $- vBox $- map (hLimit widthLimit . padBottom (Pad 1) . drawRecipe (Just e) inv) recipes- ]+ vBox+ [ padBottom (Pad 1) (hBorderWithLabel (txt "Recipes"))+ , padLeftRight 2 $+ hCenter $+ vBox $+ map (hLimit widthLimit . padBottom (Pad 1) . drawRecipe (Just e) inv) recipes+ ] where recipes = recipesWith s e @@ -1059,11 +1015,11 @@ connector | null reqs = hLimit 5 hBorder | otherwise =- hBox- [ hLimit 2 hBorder- , joinableBorder (Edges True False True True)- , hLimit 2 hBorder- ]+ hBox+ [ hLimit 2 hBorder+ , joinableBorder (Edges True False True True)+ , hLimit 2 hBorder+ ] inLen = length ins + length times outLen = length outs times = [(fromIntegral time, timeE) | time /= 1]@@ -1127,18 +1083,13 @@ drawRobotLog s = vBox [ padBottom (Pad 1) (hBorderWithLabel (txt "Log"))- , vBox . imap drawEntry $ logEntries+ , vBox . F.toList . imap drawEntry $ logEntries ] where- logEntries =- s- & view (gameState . to focusedRobot . _Just . robotLog)- & Seq.sort- & F.toList- & uniq+ logEntries = s ^. gameState . to focusedRobot . _Just . robotLog rn = s ^? gameState . to focusedRobot . _Just . robotName- n = length logEntries+ n = Seq.length logEntries allMe = all ((== rn) . Just . view leRobotName) logEntries @@ -1151,7 +1102,7 @@ drawLogEntry addName e = withAttr (colorLogs e) . txtWrapWith indent2 $ if addName then name else t where t = e ^. leText- name = "[" <> view leRobotName e <> "] " <> (if e ^. leSaid == Said then "said " <> quote t else t)+ name = "[" <> view leRobotName e <> "] " <> (if e ^. leSource == Said then "said " <> quote t else t) ------------------------------------------------------------ -- REPL panel@@ -1177,7 +1128,7 @@ replE = renderEditor (color . vBox . map txt)- (focusGetCurrent focus `elem` [Nothing, Just REPLPanel, Just REPLInput])+ (focusGetCurrent focus `elem` [Nothing, Just (FocusablePanel REPLPanel), Just REPLInput]) replEditor -- | Draw the REPL.@@ -1196,12 +1147,3 @@ base = s ^. gameState . robotMap . at 0 fmt (REPLEntry e) = txt $ "> " <> e fmt (REPLOutput t) = txt t----------------------------------------------------------------- Utility----------------------------------------------------------------- | Display a list of text-wrapped paragraphs with one blank line after--- each.-displayParagraphs :: [Text] -> Widget Name-displayParagraphs = vBox . map (padBottom (Pad 1) . txtWrap)
+ src/Swarm/TUI/View/Achievement.hs view
@@ -0,0 +1,103 @@+{-# LANGUAGE OverloadedStrings #-}++module Swarm.TUI.View.Achievement where++import Brick+import Brick.Widgets.Border (borderWithLabel)+import Brick.Widgets.Center (hCenter)+import Brick.Widgets.List qualified as BL+import Control.Lens ((^.))+import Data.Map (Map)+import Data.Map qualified as M+import Data.Time.Format (defaultTimeLocale, formatTime)+import Swarm.TUI.Attr+import Swarm.TUI.Model+import Swarm.TUI.Model.Achievement.Attainment+import Swarm.TUI.Model.Achievement.Definitions+import Swarm.TUI.Model.Achievement.Description+import Swarm.TUI.Model.UI+import Text.Wrap++padAllEvenly :: Int -> Widget Name -> Widget Name+padAllEvenly x w = padTopBottom x $ padLeftRight (2 * x) w++getCompletionIcon :: Bool -> Widget Name+getCompletionIcon = \case+ False -> txt " ○ "+ True -> withAttr greenAttr $ txt " ● "++drawAchievementsMenuUI :: AppState -> BL.List Name CategorizedAchievement -> Widget Name+drawAchievementsMenuUI s l =+ vBox+ [ hCenter $ padTopBottom 1 $ str "🏆 Achievements 🏆 "+ , hCenter $+ hBox+ [ hLimitPercent 30 $+ padAll 2 $+ BL.renderList (const $ drawAchievementListItem attainedMap) True l+ , hLimitPercent 50 $+ maybe emptyWidget (singleAchievementDetails attainedMap . snd) $+ BL.listSelectedElement l+ ]+ ]+ where+ attainedMap = s ^. uiState . uiAchievements++drawAchievementListItem ::+ Map CategorizedAchievement Attainment ->+ CategorizedAchievement ->+ Widget Name+drawAchievementListItem attainedMap x =+ getCompletionIcon wasAttained <+> titleWidget+ where+ wasAttained = M.member x attainedMap+ titleWidget = txtWrap $ title details+ details = describe x++singleAchievementDetails ::+ Map CategorizedAchievement Attainment ->+ CategorizedAchievement ->+ Widget Name+singleAchievementDetails attainedMap x =+ padRight (Pad 1) $ borderWithLabel titleWidget $ padAllEvenly 1 innerContent+ where+ wasAttained = M.member x attainedMap++ renderFlavorTextWidget :: FlavorText -> Widget Name+ renderFlavorTextWidget (Freeform t) = txtWrap t+ renderFlavorTextWidget (FTQuotation (Quotation author quoteContent)) =+ vBox+ [ txtWrap quoteContent+ , padLeft Max $+ padRight (Pad 2) $+ txtWrapWith (defaultWrapSettings {fillStrategy = FillIndent 2}) $+ "--" <> author+ ]++ innerContent =+ vBox+ [ maybe emptyWidget (padAllEvenly 2 . renderFlavorTextWidget) $ humorousElaboration details+ , txtWrap $+ if wasAttained || not (isObfuscated details)+ then attainmentProcess details+ else "???"+ , case M.lookup x attainedMap of+ Nothing -> emptyWidget+ Just attainment ->+ padTop (Pad 1) $+ hBox+ [ txt "Obtained: "+ , withAttr cyanAttr $+ str $+ formatTime defaultTimeLocale "%l:%M%P on %b %e, %Y" $+ attainment ^. obtainedAt+ ]+ , padTop (Pad 1) $+ hBox+ [ txt "Effort: "+ , withAttr boldAttr $ str $ show $ effort details+ ]+ ]++ titleWidget = padLeftRight 1 $ txt $ title details+ details = describe x
+ src/Swarm/TUI/View/CellDisplay.hs view
@@ -0,0 +1,59 @@+module Swarm.TUI.View.CellDisplay where++import Brick+import Control.Lens+import Data.List.NonEmpty qualified as NE+import Data.Map qualified as M+import Data.Maybe (maybeToList)+import Data.Semigroup (sconcat)+import Swarm.Game.Display+import Swarm.Game.Entity as E+import Swarm.Game.Robot+import Swarm.Game.State+import Swarm.Game.Terrain (terrainMap)+import Swarm.Game.World qualified as W+import Swarm.TUI.Model.Name++-- | Render the 'Display' for a specific location.+drawLoc :: Bool -> GameState -> W.Coords -> Widget Name+drawLoc showRobots g = renderDisplay . displayLoc showRobots g++displayTerrainCell :: GameState -> W.Coords -> Display+displayTerrainCell g coords = terrainMap M.! toEnum (W.lookupTerrain coords (g ^. world))++displayEntityCell, displayRobotCell :: GameState -> W.Coords -> [Display]+displayRobotCell g coords = map (view robotDisplay) (robotsAtLocation (W.coordsToLoc coords) g)+displayEntityCell g coords = maybeToList (displayForEntity <$> W.lookupEntity coords (g ^. world))+ where+ displayForEntity :: Entity -> Display+ displayForEntity e = (if known e then id else hidden) (e ^. entityDisplay)++ known e =+ e+ `hasProperty` Known+ || (e ^. entityName)+ `elem` (g ^. knownEntities)+ || case hidingMode g of+ HideAllEntities -> False+ HideNoEntity -> True+ HideEntityUnknownTo ro -> ro `robotKnows` e++-- | Get the 'Display' for a specific location, by combining the+-- 'Display's for the terrain, entity, and robots at the location.+displayLoc :: Bool -> GameState -> W.Coords -> Display+displayLoc showRobots g coords =+ sconcat $ terrain NE.:| entity <> robots+ where+ terrain = displayTerrainCell g coords+ entity = displayEntityCell g coords+ robots =+ if showRobots+ then displayRobotCell g coords+ else []++data HideEntity = HideAllEntities | HideNoEntity | HideEntityUnknownTo Robot++hidingMode :: GameState -> HideEntity+hidingMode g+ | g ^. creativeMode = HideNoEntity+ | otherwise = maybe HideAllEntities HideEntityUnknownTo $ focusedRobot g
+ src/Swarm/TUI/View/CustomStyling.hs view
@@ -0,0 +1,37 @@+module Swarm.TUI.View.CustomStyling where++import Brick (AttrName, attrName)+import Data.Set (toList)+import Data.Text qualified as T+import Graphics.Vty.Attributes+import Numeric (readHex)+import Swarm.Game.Scenario.Style+import Swarm.TUI.Attr (worldPrefix)++toStyle :: StyleFlag -> Style+toStyle = \case+ Standout -> standout+ Italic -> italic+ Strikethrough -> strikethrough+ Underline -> underline+ ReverseVideo -> reverseVideo+ Blink -> blink+ Dim -> dim+ Bold -> bold++toAttrColor :: HexColor -> Color+toAttrColor (HexColor colorText) =+ case nums of+ [r, g, b] -> RGBColor r g b+ _ -> RGBColor 255 255 255+ where+ chunks = T.chunksOf 2 $ T.dropWhile (== '#') colorText+ nums = map (fst . head . readHex . T.unpack) chunks++toAttrPair :: CustomAttr -> (AttrName, Attr)+toAttrPair ca =+ (worldPrefix <> attrName (name ca), addStyle $ addFg $ addBg currentAttr)+ where+ addFg = maybe id (flip withForeColor . toAttrColor) $ fg ca+ addBg = maybe id (flip withBackColor . toAttrColor) $ bg ca+ addStyle = maybe id (flip withStyle . sum . map toStyle . toList) $ style ca
+ src/Swarm/TUI/View/Util.hs view
@@ -0,0 +1,159 @@+{-# LANGUAGE OverloadedStrings #-}++module Swarm.TUI.View.Util where++import Brick hiding (Direction)+import Brick.Widgets.Dialog+import Brick.Widgets.List qualified as BL+import Control.Lens hiding (Const, from)+import Control.Monad.Reader (withReaderT)+import Data.List.NonEmpty (NonEmpty (..))+import Data.Maybe (catMaybes, fromMaybe)+import Data.Text (Text)+import Data.Text qualified as T+import Graphics.Vty qualified as V+import Swarm.Game.Entity as E+import Swarm.Game.Scenario (scenarioName)+import Swarm.Game.ScenarioInfo (scenarioItemName)+import Swarm.Game.State+import Swarm.Language.Pretty (prettyText)+import Swarm.Language.Types (Polytype)+import Swarm.TUI.Attr+import Swarm.TUI.Model+import Swarm.TUI.Model.UI+import Witch (from, into)++-- | Generate a fresh modal window of the requested type.+generateModal :: AppState -> ModalType -> Modal+generateModal s mt = Modal mt (dialog (Just $ str title) buttons (maxModalWindowWidth `min` requiredWidth))+ where+ currentScenario = s ^. uiState . scenarioRef+ currentSeed = s ^. gameState . seed+ haltingMessage = case s ^. uiState . uiMenu of+ NoMenu -> Just "Quit"+ _ -> Nothing+ descriptionWidth = 100+ helpWidth = 80+ (title, buttons, requiredWidth) =+ case mt of+ HelpModal -> (" Help ", Nothing, helpWidth)+ RobotsModal -> ("Robots", Nothing, descriptionWidth)+ RecipesModal -> ("Available Recipes", Nothing, descriptionWidth)+ CommandsModal -> ("Available Commands", Nothing, descriptionWidth)+ MessagesModal -> ("Messages", Nothing, descriptionWidth)+ WinModal ->+ let nextMsg = "Next challenge!"+ stopMsg = fromMaybe "Return to the menu" haltingMessage+ continueMsg = "Keep playing"+ in ( ""+ , Just+ ( Button NextButton+ , [ (nextMsg, Button NextButton, Next scene)+ | Just scene <- [nextScenario (s ^. uiState . uiMenu)]+ ]+ ++ [ (stopMsg, Button QuitButton, QuitAction)+ , (continueMsg, Button KeepPlayingButton, KeepPlaying)+ ]+ )+ , sum (map length [nextMsg, stopMsg, continueMsg]) + 32+ )+ LoseModal ->+ let stopMsg = fromMaybe "Return to the menu" haltingMessage+ continueMsg = "Keep playing"+ maybeStartOver = do+ cs <- currentScenario+ return ("Start over", Button StartOverButton, StartOver currentSeed cs)+ in ( ""+ , Just+ ( Button QuitButton+ , catMaybes+ [ Just (stopMsg, Button QuitButton, QuitAction)+ , maybeStartOver+ , Just (continueMsg, Button KeepPlayingButton, KeepPlaying)+ ]+ )+ , sum (map length [stopMsg, continueMsg]) + 32+ )+ DescriptionModal e -> (descriptionTitle e, Nothing, descriptionWidth)+ QuitModal ->+ let stopMsg = fromMaybe ("Quit to" ++ maybe "" (" " ++) (into @String <$> curMenuName s) ++ " menu") haltingMessage+ maybeStartOver = do+ cs <- currentScenario+ return ("Start over", Button StartOverButton, StartOver currentSeed cs)+ in ( ""+ , Just+ ( Button CancelButton+ , catMaybes+ [ Just ("Keep playing", Button CancelButton, Cancel)+ , maybeStartOver+ , Just (stopMsg, Button QuitButton, QuitAction)+ ]+ )+ , T.length (quitMsg (s ^. uiState . uiMenu)) + 4+ )+ GoalModal ->+ let goalModalTitle = case currentScenario of+ Nothing -> "Goal"+ Just (scenario, _) -> scenario ^. scenarioName+ in (" " <> T.unpack goalModalTitle <> " ", Nothing, descriptionWidth)+ KeepPlayingModal -> ("", Just (Button CancelButton, [("OK", Button CancelButton, Cancel)]), 80)++-- | Render the type of the current REPL input to be shown to the user.+drawType :: Polytype -> Widget Name+drawType = withAttr infoAttr . padLeftRight 1 . txt . prettyText++descriptionTitle :: Entity -> String+descriptionTitle e = " " ++ from @Text (e ^. entityName) ++ " "++-- | Width cap for modal and error message windows+maxModalWindowWidth :: Int+maxModalWindowWidth = 500++-- | Get the name of the current New Game menu.+curMenuName :: AppState -> Maybe Text+curMenuName s = case s ^. uiState . uiMenu of+ NewGameMenu (_ :| (parentMenu : _)) ->+ Just (parentMenu ^. BL.listSelectedElementL . to scenarioItemName)+ NewGameMenu _ -> Just "Scenarios"+ _ -> Nothing++quitMsg :: Menu -> Text+quitMsg m = "Are you sure you want to " <> quitAction <> "? All progress on this scenario will be lost!"+ where+ quitAction = case m of+ NoMenu -> "quit"+ _ -> "return to the menu"++-- | Display a list of text-wrapped paragraphs with one blank line after+-- each.+displayParagraphs :: [Text] -> Widget Name+displayParagraphs = vBox . map (padBottom (Pad 1) . txtWrap)++withEllipsis :: Text -> Widget Name+withEllipsis t =+ Widget Greedy Fixed $ do+ ctx <- getContext+ let w = ctx ^. availWidthL+ ellipsis = T.replicate 3 $ T.singleton '.'+ tLength = T.length t+ newText =+ if tLength > w+ then T.take (w - T.length ellipsis) t <> ellipsis+ else t+ render $ txt newText++-- | Make a widget scrolling if it is bigger than the available+-- vertical space. Thanks to jtdaugherty for this code.+maybeScroll :: (Ord n, Show n) => n -> Widget n -> Widget n+maybeScroll vpName contents =+ Widget Greedy Greedy $ do+ ctx <- getContext+ result <- withReaderT (availHeightL .~ 10000) (render contents)+ if V.imageHeight (result ^. imageL) <= ctx ^. availHeightL+ then return result+ else+ render $+ withVScrollBars OnRight $+ viewport vpName Vertical $+ Widget Fixed Fixed $+ return result
src/Swarm/Util.hs view
@@ -17,17 +17,23 @@ maxOn, maximum0, cycleEnum,+ listEnums, uniq, getElemsInArea, manhattan,+ binTuples, -- * Directory utilities readFileMay, readFileMayT,- getSwarmDataPath,+ getSwarmXdgDataSubdir,+ getSwarmXdgDataFile, getSwarmSavePath, getSwarmHistoryPath, readAppData,+ getDataDirSafe,+ getDataFileNameSafe,+ dataNotFound, -- * Text utilities isIdentChar,@@ -37,6 +43,7 @@ reflow, quote, squote,+ bquote, commaList, indefinite, indefiniteQ,@@ -63,9 +70,6 @@ -- * Utilities for NP-hard approximation smallHittingSet,- getDataDirSafe,- getDataFileNameSafe,- dataNotFound, ) where import Control.Algebra (Has)@@ -76,13 +80,13 @@ import Control.Lens (ASetter', Lens', LensLike, LensLike', Over, lens, (<>~)) import Control.Lens.Lens ((&)) import Control.Monad (forM, unless, when)-import Data.Aeson (FromJSONKey, ToJSONKey) import Data.Bifunctor (first) import Data.Char (isAlphaNum) import Data.Either.Validation-import Data.Int (Int64)+import Data.Int (Int32) import Data.List (maximumBy, partition) import Data.List.NonEmpty (NonEmpty (..))+import Data.List.NonEmpty qualified as NE import Data.Map (Map) import Data.Map qualified as M import Data.Maybe (fromMaybe, mapMaybe)@@ -96,10 +100,10 @@ import Data.Yaml import Language.Haskell.TH import Language.Haskell.TH.Syntax (lift)-import Linear (V2 (V2)) import NLP.Minimorph.English qualified as MM import NLP.Minimorph.Util ((<+>)) import Paths_swarm (getDataDir)+import Swarm.Util.Location import System.Clock (TimeSpec) import System.Directory ( XdgDirectory (XdgData),@@ -116,7 +120,7 @@ -- $setup -- >>> import qualified Data.Map as M--- >>> import Linear.V2+-- >>> import Swarm.Util.Location infixr 1 ? infix 4 %%=, <+=, <%=, <<.=, <>=@@ -148,6 +152,9 @@ | e == maxBound = minBound | otherwise = succ e +listEnums :: (Enum e, Bounded e) => [e]+listEnums = [minBound .. maxBound]+ -- | Drop repeated elements that are adjacent to each other. -- -- >>> uniq []@@ -164,21 +171,21 @@ (x : xs) -> x : uniq (dropWhile (== x) xs) -- | Manhattan distance between world locations.-manhattan :: V2 Int64 -> V2 Int64 -> Int64-manhattan (V2 x1 y1) (V2 x2 y2) = abs (x1 - x2) + abs (y1 - y2)+manhattan :: Location -> Location -> Int32+manhattan (Location x1 y1) (Location x2 y2) = abs (x1 - x2) + abs (y1 - y2) -- | Get elements that are in manhattan distance from location. ----- >>> v2s i = [(v, manhattan (V2 0 0) v) | x <- [-i..i], y <- [-i..i], let v = V2 x y]+-- >>> v2s i = [(p, manhattan origin p) | x <- [-i..i], y <- [-i..i], let p = Location x y] -- >>> v2s 0--- [(V2 0 0,0)]--- >>> map (\i -> length (getElemsInArea (V2 0 0) i (M.fromList $ v2s i))) [0..8]+-- [(P (V2 0 0),0)]+-- >>> map (\i -> length (getElemsInArea origin i (M.fromList $ v2s i))) [0..8] -- [1,5,13,25,41,61,85,113,145] -- -- The last test is the sequence "Centered square numbers": -- https://oeis.org/A001844-getElemsInArea :: V2 Int64 -> Int64 -> Map (V2 Int64) e -> [e]-getElemsInArea o@(V2 x y) d m = M.elems sm'+getElemsInArea :: Location -> Int32 -> Map Location e -> [e]+getElemsInArea o@(Location x y) d m = M.elems sm' where -- to be more efficient we basically split on first coordinate -- (which is logarithmic) and then we have to linearly filter@@ -194,12 +201,22 @@ -- ▼▼▼▼ sm = m- & M.split (V2 (x - d) (y - 1)) -- A+ & M.split (Location (x - d) (y - 1)) -- A & snd -- A<- & M.split (V2 (x + d) (y + 1)) -- B+ & M.split (Location (x + d) (y + 1)) -- B & fst -- B> sm' = M.filterWithKey (const . (<= d) . manhattan o) sm +-- | Place the second element of the tuples into bins by+-- the value of the first element.+binTuples ::+ (Foldable t, Ord a) =>+ t (a, b) ->+ Map a (NE.NonEmpty b)+binTuples = foldr f mempty+ where+ f = uncurry (M.insertWith (<>)) . fmap pure+ ------------------------------------------------------------ -- Directory stuff @@ -215,23 +232,33 @@ catchIO :: IO a -> IO (Maybe a) catchIO act = (Just <$> act) `catchIOError` (\_ -> return Nothing) +-- | Get subdirectory from swarm data directory.+--+-- This will first look in Cabal generated path and then+-- try a `data` directory in 'XdgData' path.+--+-- The idea is that when installing with Cabal/Stack the first+-- is preferred, but when the players install a binary they+-- need to extract the `data` archive to the XDG directory. getDataDirSafe :: FilePath -> IO (Maybe FilePath) getDataDirSafe p = do- d <- mySubdir <$> getDataDir+ d <- (`appDir` p) <$> getDataDir de <- doesDirectoryExist d if de then return $ Just d else do- xd <- mySubdir . (</> "data") <$> getSwarmDataPath False+ xd <- (`appDir` p) <$> getSwarmXdgDataSubdir False "data" xde <- doesDirectoryExist xd return $ if xde then Just xd else Nothing where- mySubdir d = d `appDir` p appDir r = \case "" -> r "." -> r d -> r </> d +-- | Get file from swarm data directory.+--+-- See the note in 'getDataDirSafe'. getDataFileNameSafe :: FilePath -> IO (Maybe FilePath) getDataFileNameSafe name = do dir <- getDataDirSafe "."@@ -242,38 +269,41 @@ fe <- doesFileExist fp return $ if fe then Just fp else Nothing +-- | Get a nice message suggesting to download `data` directory to 'XdgData'. dataNotFound :: FilePath -> IO Text dataNotFound f = do- d <- getSwarmDataPath False+ d <- getSwarmXdgDataSubdir False "" let squotes = squote . T.pack return $ T.unlines [ "Could not find the data: " <> squotes f- , "Try downloading the Swarm 'data' directory to: " <> squotes d+ , "Try downloading the Swarm 'data' directory to: " <> squotes (d </> "data") ] -- | Get path to swarm data, optionally creating necessary--- directories.-getSwarmDataPath :: Bool -> IO FilePath-getSwarmDataPath createDirs = do- swarmData <- getXdgDirectory XdgData "swarm"+-- directories. This could fail if user has bad permissions+-- on his own $HOME or $XDG_DATA_HOME which is unlikely.+getSwarmXdgDataSubdir :: Bool -> FilePath -> IO FilePath+getSwarmXdgDataSubdir createDirs subDir = do+ swarmData <- (</> subDir) <$> getXdgDirectory XdgData "swarm" when createDirs (createDirectoryIfMissing True swarmData) pure swarmData +getSwarmXdgDataFile :: Bool -> FilePath -> IO FilePath+getSwarmXdgDataFile createDirs filepath = do+ let (subDir, file) = splitFileName filepath+ d <- getSwarmXdgDataSubdir createDirs subDir+ return $ d </> file+ -- | Get path to swarm saves, optionally creating necessary -- directories. getSwarmSavePath :: Bool -> IO FilePath-getSwarmSavePath createDirs = do- swarmSave <- getXdgDirectory XdgData ("swarm" </> "saves")- when createDirs (createDirectoryIfMissing True swarmSave)- pure swarmSave+getSwarmSavePath createDirs = getSwarmXdgDataSubdir createDirs "saves" -- | Get path to swarm history, optionally creating necessary--- directories. This could fail if user has bad permissions--- on his own $HOME or $XDG_DATA_HOME which is unlikely.+-- directories. getSwarmHistoryPath :: Bool -> IO FilePath-getSwarmHistoryPath createDirs =- (</> "history") <$> getSwarmDataPath createDirs+getSwarmHistoryPath createDirs = getSwarmXdgDataFile createDirs "history" -- | Read all the .txt files in the data/ directory. readAppData :: IO (Map Text Text)@@ -346,9 +376,9 @@ singularSubjectVerb :: Text -> Text -> Text singularSubjectVerb sub verb | verb == "be" = case toUpper sub of- "I" -> "I am"- "YOU" -> sub <+> "are"- _ -> sub <+> "is"+ "I" -> "I am"+ "YOU" -> sub <+> "are"+ _ -> sub <+> "is" | otherwise = sub <+> (if is3rdPerson then verb3rd else verb) where is3rdPerson = toUpper sub `notElem` ["I", "YOU"]@@ -379,6 +409,10 @@ quote :: Text -> Text quote t = T.concat ["\"", t, "\""] +-- | Surround some text in backticks.+bquote :: Text -> Text+bquote t = T.concat ["`", t, "`"]+ -- | Make a list of things with commas and the word "and". commaList :: [Text] -> Text commaList [] = ""@@ -388,12 +422,6 @@ ------------------------------------------------------------ -- Some orphan instances--deriving instance ToJSON (V2 Int64)-deriving instance FromJSON (V2 Int64)--deriving instance FromJSONKey (V2 Int64)-deriving instance ToJSONKey (V2 Int64) deriving instance FromJSON TimeSpec deriving instance ToJSON TimeSpec
+ src/Swarm/Util/Location.hs view
@@ -0,0 +1,59 @@+{-# LANGUAGE PatternSynonyms #-}+{-# OPTIONS_GHC -fno-warn-orphans #-}++-- Orphan JSON instances for Location and Heading++-- |+-- Module : Swarm.Util.Location+-- Copyright : Brent Yorgey+-- Maintainer : byorgey@gmail.com+--+-- SPDX-License-Identifier: BSD-3-Clause+--+-- Locations and headings.+module Swarm.Util.Location (Location, pattern Location, Heading, origin, Affine (..)) where++import Data.Aeson (FromJSONKey, ToJSONKey)+import Data.Int (Int32)+import Data.Yaml (FromJSON (parseJSON), ToJSON (toJSON))+import Linear (V2 (..))+import Linear.Affine (Affine (..), Point (..), origin)++-- | A Location is a pair of (x,y) coordinates, both up to 32 bits.+-- The positive x-axis points east and the positive y-axis points+-- north. These are the coordinates that are shown to players.+--+-- See also the 'Coords' type defined in "Swarm.Game.World", which+-- use a (row, column) format instead, which is more convenient for+-- internal use. The "Swarm.Game.World" module also defines+-- conversions between 'Location' and 'Coords'.+type Location = Point V2 Int32++-- | A convenient way to pattern-match on 'Location' values.+pattern Location :: Int32 -> Int32 -> Location+pattern Location x y = P (V2 x y)++{-# COMPLETE Location #-}++-- | A @Heading@ is a 2D vector, with 32-bit coordinates.+--+-- 'Location' and 'Heading' are both represented using types from+-- the @linear@ package, so they can be manipulated using a large+-- number of operators from that package. For example:+--+-- * Two headings can be added with '^+^'.+-- * The difference between two 'Location's is a 'Heading' (via '.-.').+-- * A 'Location' plus a 'Heading' is another 'Location' (via '.^+').+type Heading = V2 Int32++deriving instance ToJSON (V2 Int32)+deriving instance FromJSON (V2 Int32)++deriving instance FromJSONKey (V2 Int32)+deriving instance ToJSONKey (V2 Int32)++instance FromJSON Location where+ parseJSON = fmap P . parseJSON++instance ToJSON Location where+ toJSON (P v) = toJSON v
src/Swarm/Web.hs view
@@ -25,41 +25,87 @@ import Control.Lens ((^.)) import Control.Monad (void) import Control.Monad.IO.Class (liftIO)+import Data.Foldable (toList) import Data.IORef (IORef, readIORef) import Data.IntMap qualified as IM import Data.Maybe (fromMaybe)+import Data.Text qualified as T import Network.Wai qualified import Network.Wai.Handler.Warp qualified as Warp import Servant import Swarm.Game.Robot+import Swarm.Game.Scenario.Objective+import Swarm.Game.Scenario.Objective.Graph+import Swarm.Game.Scenario.Objective.Presentation.Model+import Swarm.Game.Scenario.Objective.WinCheck import Swarm.Game.State+import Swarm.TUI.Model+import Swarm.TUI.Model.UI import System.Timeout (timeout) type SwarmApi = "robots" :> Get '[JSON] [Robot] :<|> "robot" :> Capture "id" Int :> Get '[JSON] (Maybe Robot)+ :<|> "goals" :> "prereqs" :> Get '[JSON] [PrereqSatisfaction]+ :<|> "goals" :> "active" :> Get '[JSON] [Objective]+ :<|> "goals" :> "graph" :> Get '[JSON] (Maybe GraphInfo)+ :<|> "goals" :> "uigoal" :> Get '[JSON] GoalTracking+ :<|> "goals" :> Get '[JSON] WinCondition+ :<|> "repl" :> "history" :> "full" :> Get '[JSON] [T.Text] -mkApp :: IORef GameState -> Servant.Server SwarmApi-mkApp gsRef =+mkApp :: IORef AppState -> Servant.Server SwarmApi+mkApp appStateRef = robotsHandler :<|> robotHandler+ :<|> prereqsHandler+ :<|> activeGoalsHandler+ :<|> goalsGraphHandler+ :<|> uiGoalHandler+ :<|> goalsHandler+ :<|> replHandler where robotsHandler = do- g <- liftIO (readIORef gsRef)- pure $ IM.elems $ g ^. robotMap+ appState <- liftIO (readIORef appStateRef)+ pure $ IM.elems $ appState ^. gameState . robotMap robotHandler rid = do- g <- liftIO (readIORef gsRef)- pure $ IM.lookup rid (g ^. robotMap)+ appState <- liftIO (readIORef appStateRef)+ pure $ IM.lookup rid (appState ^. gameState . robotMap)+ prereqsHandler = do+ appState <- liftIO (readIORef appStateRef)+ case appState ^. gameState . winCondition of+ WinConditions _winState oc -> return $ getSatisfaction oc+ _ -> return []+ activeGoalsHandler = do+ appState <- liftIO (readIORef appStateRef)+ case appState ^. gameState . winCondition of+ WinConditions _winState oc -> return $ getActiveObjectives oc+ _ -> return []+ goalsGraphHandler = do+ appState <- liftIO (readIORef appStateRef)+ return $ case appState ^. gameState . winCondition of+ WinConditions _winState oc -> Just $ makeGraphInfo oc+ _ -> Nothing+ uiGoalHandler = do+ appState <- liftIO (readIORef appStateRef)+ return $ appState ^. uiState . uiGoal . goalsContent+ goalsHandler = do+ appState <- liftIO (readIORef appStateRef)+ return $ appState ^. gameState . winCondition+ replHandler = do+ appState <- liftIO (readIORef appStateRef)+ let replHistorySeq = appState ^. uiState . uiREPL . replHistory . replSeq+ items = [x | REPLEntry x <- toList replHistorySeq]+ pure items -webMain :: Maybe (MVar (Either String ())) -> Warp.Port -> IORef GameState -> IO ()-webMain baton port gsRef = catch (Warp.runSettings settings app) handleErr+webMain :: Maybe (MVar (Either String ())) -> Warp.Port -> IORef AppState -> IO ()+webMain baton port appStateRef = catch (Warp.runSettings settings app) handleErr where settings = Warp.setPort port $ onReady Warp.defaultSettings onReady = case baton of Just mv -> Warp.setBeforeMainLoop $ putMVar mv (Right ()) Nothing -> id app :: Network.Wai.Application- app = Servant.serve (Proxy @SwarmApi) (mkApp gsRef)+ app = Servant.serve (Proxy @SwarmApi) (mkApp appStateRef) handleErr :: IOException -> IO () handleErr e = case baton of Just mv -> putMVar mv (Left $ displayException e)@@ -74,13 +120,13 @@ -- startup doesn't work. Otherwise, ignore the failure. In any -- case, return a @Maybe Port@ value representing whether a web -- server is actually running, and if so, what port it is on.-startWebThread :: Maybe Warp.Port -> IORef GameState -> IO (Either String Warp.Port)+startWebThread :: Maybe Warp.Port -> IORef AppState -> IO (Either String Warp.Port) -- User explicitly provided port '0': don't run the web server startWebThread (Just 0) _ = pure $ Left "The web port has been turned off."-startWebThread portM gsRef = do+startWebThread portM appStateRef = do baton <- newEmptyMVar let port = fromMaybe defaultPort portM- void $ forkIO $ webMain (Just baton) port gsRef+ void $ forkIO $ webMain (Just baton) port appStateRef res <- timeout 500_000 (takeMVar baton) case (portM, res) of -- User requested explicit port but server didn't start: fail
swarm.cabal view
@@ -1,6 +1,6 @@ cabal-version: 2.4 name: swarm-version: 0.2.0.0+version: 0.3.0.0 synopsis: 2D resource gathering game with programmable robots description: Swarm is a 2D programming and resource gathering@@ -24,7 +24,7 @@ editors/emacs/*.el editors/vscode/syntaxes/*.json data-dir: data/-data-files: *.yaml, scenarios/**/*.yaml, scenarios/**/*.txt, scenarios/**/*.sw, *.txt+data-files: *.yaml, scenarios/**/*.yaml, scenarios/**/*.txt, scenarios/**/*.sw, *.txt, test/language-snippets/**/*.sw source-repository head type: git@@ -81,55 +81,88 @@ library import: stan-config, common, ghc2021-extensions- exposed-modules: Swarm.Language.Context- Swarm.Language.Types- Swarm.Language.Syntax- Swarm.Language.Capability- Swarm.Language.Requirement- Swarm.Language.Parse- Swarm.Language.Parse.QQ- Swarm.Language.Pretty- Swarm.Language.Typecheck- Swarm.Language.Typed- Swarm.Language.Elaborate- Swarm.Language.LSP- Swarm.Language.Pipeline- Swarm.Language.Pipeline.QQ+ exposed-modules: Data.BoolExpr+ Data.BoolExpr.Simplify+ Swarm.App+ Swarm.DocGen Swarm.Game.CESK- Swarm.Game.Scenario- Swarm.Game.ScenarioInfo Swarm.Game.Display Swarm.Game.Entity Swarm.Game.Exception+ Swarm.Game.Log Swarm.Game.Recipe Swarm.Game.Robot+ Swarm.Game.Scenario+ Swarm.Game.Scenario.Cell+ Swarm.Game.Scenario.Objective.Logic+ Swarm.Game.Scenario.Objective.Graph+ Swarm.Game.Scenario.Objective.Presentation.Model+ Swarm.Game.Scenario.Objective.Presentation.Render+ Swarm.Game.Scenario.Objective.Validation+ Swarm.Game.Scenario.Objective.WinCheck+ Swarm.Game.Scenario.Objective+ Swarm.Game.Scenario.RobotLookup+ Swarm.Game.Scenario.Style+ Swarm.Game.Scenario.WorldDescription+ Swarm.Game.ScenarioInfo Swarm.Game.State Swarm.Game.Step Swarm.Game.Terrain- Swarm.Game.Value Swarm.Game.World Swarm.Game.WorldGen+ Swarm.Language.Context+ Swarm.Language.Capability+ Swarm.Language.Elaborate+ Swarm.Language.LSP+ Swarm.Language.LSP.Hover+ Swarm.Language.LSP.VarUsage+ Swarm.Language.Module+ Swarm.Language.Parse+ Swarm.Language.Parse.QQ+ Swarm.Language.Pipeline+ Swarm.Language.Pipeline.QQ+ Swarm.Language.Pretty+ Swarm.Language.Requirement+ Swarm.Language.Syntax+ Swarm.Language.Typecheck+ Swarm.Language.Typed+ Swarm.Language.Types+ Swarm.Language.Value Swarm.TUI.Attr Swarm.TUI.Border+ Swarm.TUI.Controller+ Swarm.TUI.Controller.Util+ Swarm.TUI.View.CustomStyling+ Swarm.TUI.Inventory.Sorting Swarm.TUI.List- Swarm.TUI.Panel Swarm.TUI.Model+ Swarm.TUI.Model.Achievement.Attainment+ Swarm.TUI.Model.Achievement.Definitions+ Swarm.TUI.Model.Achievement.Description+ Swarm.TUI.Model.Achievement.Persistence+ Swarm.TUI.Model.Failure+ Swarm.TUI.Model.Menu+ Swarm.TUI.Model.Name+ Swarm.TUI.Model.Repl+ Swarm.TUI.Model.StateUpdate+ Swarm.TUI.Model.UI+ Swarm.TUI.Panel Swarm.TUI.View- Swarm.TUI.Controller- Swarm.TUI.Inventory.Sorting- Swarm.App- Swarm.Version- Swarm.Web+ Swarm.TUI.View.Achievement+ Swarm.TUI.View.CellDisplay+ Swarm.TUI.View.Util Swarm.Util- Swarm.DocGen+ Swarm.Util.Location Swarm.Util.Yaml+ Swarm.Version+ Swarm.Web other-modules: Paths_swarm autogen-modules: Paths_swarm build-depends: base >= 4.14 && < 4.18, aeson >= 2 && < 2.2, array >= 0.5.4 && < 0.6,- brick >= 1.0 && < 1.4,+ brick >= 1.5 && < 1.7, bytestring >= 0.10 && < 0.12, clock >= 0.8.2 && < 0.9, containers >= 0.6.2 && < 0.7,@@ -147,9 +180,9 @@ http-client-tls >= 0.3 && < 0.4, http-types >= 0.12 && < 0.13, lens >= 4.19 && < 5.3,- linear >= 1.21.6 && < 1.22,- lsp >= 1.2 && < 1.7,- megaparsec >= 9.0 && < 9.3,+ linear >= 1.21.6 && < 1.23,+ lsp >= 1.6 && < 1.7,+ megaparsec >= 9.0 && < 9.4, minimorph >= 0.3 && < 0.4, mtl >= 2.2.2 && < 2.3, murmur3 >= 1.0.4 && < 1.1,@@ -166,6 +199,7 @@ tagged >= 0.8 && < 0.9, template-haskell >= 2.16 && < 2.20, text >= 1.2.4 && < 2.1,+ text-rope >= 0.2 && < 0.3, text-zipper >= 0.10 && < 0.13, time >= 1.9 && < 1.14, unification-fd >= 0.11 && < 0.12,@@ -208,6 +242,8 @@ TestNotification TestLanguagePipeline TestPretty+ TestBoolExpr+ TestLSP TestUtil build-depends: tasty >= 0.10 && < 1.5,
test/integration/Main.hs view
@@ -5,16 +5,18 @@ -- | Swarm integration tests module Main where -import Control.Lens (Ixed (ix), to, use, view, (&), (.~), (<&>), (^.), (^?!))-import Control.Monad (filterM, forM_, unless, void, when)+import Control.Lens (Ixed (ix), to, use, view, (&), (.~), (<&>), (<>~), (^.), (^..), (^?!))+import Control.Monad (filterM, forM_, unless, when) import Control.Monad.State (StateT (runStateT), gets) import Control.Monad.Trans.Except (runExceptT) import Data.Char (isSpace) import Data.Containers.ListUtils (nubOrd) import Data.Foldable (Foldable (toList), find) import Data.IntSet qualified as IS+import Data.List (partition) import Data.Map qualified as M import Data.Maybe (isJust)+import Data.Sequence (Seq) import Data.Text (Text) import Data.Text qualified as T import Data.Text.IO qualified as T@@ -23,11 +25,12 @@ import Swarm.DocGen qualified as DocGen import Swarm.Game.CESK (emptyStore, initMachine) import Swarm.Game.Entity (EntityMap, loadEntities)-import Swarm.Game.Robot (leText, machine, robotLog, waitingUntil)+import Swarm.Game.Robot (LogEntry, defReqs, leText, machine, robotContext, robotLog, waitingUntil) import Swarm.Game.Scenario (Scenario) import Swarm.Game.State ( GameState,- WinCondition (Won),+ WinCondition (WinConditions),+ WinStatus (Won), activeRobots, baseRobot, initGameStateForScenario,@@ -40,21 +43,25 @@ ) import Swarm.Game.Step (gameTick) import Swarm.Language.Context qualified as Ctx-import Swarm.Language.Pipeline (processTerm)+import Swarm.Language.Pipeline (ProcessedTerm (..), processTerm) import Swarm.Util.Yaml (decodeFileEitherE) import System.Directory (doesDirectoryExist, doesFileExist, listDirectory) import System.Environment (getEnvironment)+import System.FilePath (splitDirectories) import System.FilePath.Posix (takeExtension, (</>)) import System.Timeout (timeout) import Test.Tasty (TestTree, defaultMain, testGroup)-import Test.Tasty.ExpectedFailure (expectFailBecause) import Test.Tasty.HUnit (Assertion, assertBool, assertFailure, testCase) import Witch (into) +isUnparseableTest :: (FilePath, String) -> Bool+isUnparseableTest (fp, _) = "_Validation" `elem` splitDirectories fp+ main :: IO () main = do examplePaths <- acquire "example" "sw" scenarioPaths <- acquire "data/scenarios" "yaml"+ let (unparseableScenarios, parseableScenarios) = partition isUnparseableTest scenarioPaths scenarioPrograms <- acquire "data/scenarios" "sw" ci <- any (("CI" ==) . fst) <$> getEnvironment entities <- loadEntities@@ -66,7 +73,8 @@ "Tests" [ exampleTests examplePaths , exampleTests scenarioPrograms- , scenarioTests em scenarioPaths+ , scenarioParseTests em parseableScenarios+ , scenarioParseInvalidTests em unparseableScenarios , testScenarioSolution ci em , testEditorFiles ]@@ -81,19 +89,34 @@ where value = processTerm $ into @Text fileContent -scenarioTests :: EntityMap -> [(FilePath, String)] -> TestTree-scenarioTests em inputs = testGroup "Test scenarios" (map (scenarioTest em) inputs)+scenarioParseTests :: EntityMap -> [(FilePath, String)] -> TestTree+scenarioParseTests em inputs =+ testGroup+ "Test scenarios parse"+ (map (scenarioTest Parsed em) inputs) -scenarioTest :: EntityMap -> (FilePath, String) -> TestTree-scenarioTest em (path, _) =- testCase ("parse scenario " ++ show path) (void $ getScenario em path)+scenarioParseInvalidTests :: EntityMap -> [(FilePath, String)] -> TestTree+scenarioParseInvalidTests em inputs =+ testGroup+ "Test invalid scenarios fail to parse"+ (map (scenarioTest Failed em) inputs) -getScenario :: EntityMap -> FilePath -> IO Scenario-getScenario em p = do+data ParseResult = Parsed | Failed++scenarioTest :: ParseResult -> EntityMap -> (FilePath, String) -> TestTree+scenarioTest expRes em (path, _) =+ testCase ("parse scenario " ++ show path) (getScenario expRes em path)++getScenario :: ParseResult -> EntityMap -> FilePath -> IO ()+getScenario expRes em p = do res <- decodeFileEitherE em p :: IO (Either ParseException Scenario)- case res of- Left err -> assertFailure (prettyPrintParseException err)- Right s -> return s+ case expRes of+ Parsed -> case res of+ Left err -> assertFailure (prettyPrintParseException err)+ Right _s -> return ()+ Failed -> case res of+ Left _err -> return ()+ Right _s -> assertFailure "Unexpectedly parsed invalid scenario!" acquire :: FilePath -> String -> IO [(FilePath, String)] acquire dir ext = do@@ -124,6 +147,8 @@ sec :: Int sec = 10 ^ (6 :: Int) +data ShouldCheckBadErrors = CheckForBadErrors | AllowBadErrors deriving (Eq, Show)+ testScenarioSolution :: Bool -> EntityMap -> TestTree testScenarioSolution _ci _em = testGroup@@ -137,10 +162,10 @@ , testSolution Default "Tutorials/place" , testSolution Default "Tutorials/types" , testSolution Default "Tutorials/type-errors"- , testSolution Default "Tutorials/bind"- , testSolution Default "Tutorials/install"+ , testSolution Default "Tutorials/equip" , testSolution Default "Tutorials/build"- , testSolution' Default "Tutorials/crash" $ \g -> do+ , testSolution Default "Tutorials/bind2"+ , testSolution' Default "Tutorials/crash" CheckForBadErrors $ \g -> do let rs = toList $ g ^. robotMap let hints = any (T.isInfixOf "you will win" . view leText) . toList . view robotLog let win = isJust $ find hints rs@@ -149,7 +174,7 @@ , testSolution Default "Tutorials/def" , testSolution Default "Tutorials/lambda" , testSolution Default "Tutorials/require"- , testSolution Default "Tutorials/requireinv"+ , testSolution (Sec 3) "Tutorials/requireinv" , testSolution Default "Tutorials/conditionals" , testSolution (Sec 5) "Tutorials/farming" ]@@ -158,7 +183,10 @@ [ testSolution Default "Challenges/chess_horse" , testSolution Default "Challenges/teleport" , testSolution (Sec 5) "Challenges/2048"+ , testSolution (Sec 3) "Challenges/word-search"+ , testSolution (Sec 3) "Challenges/ice-cream" , testSolution (Sec 10) "Challenges/hanoi"+ , testSolution Default "Challenges/friend" , testGroup "Mazes" [ testSolution Default "Challenges/Mazes/easy_cave_maze"@@ -166,19 +194,22 @@ , testSolution Default "Challenges/Mazes/invisible_maze" , testSolution Default "Challenges/Mazes/loopy_maze" ]+ , testGroup+ "Ranching"+ [ testSolution (Sec 30) "Challenges/Ranching/gated-paddock"+ ] ] , testGroup "Regression tests"- [ expectFailBecause "Awaiting fix (#394)" $- testSolution Default "Testing/394-build-drill"+ [ testSolution Default "Testing/394-build-drill" , testSolution Default "Testing/373-drill" , testSolution Default "Testing/428-drowning-destroy"- , testSolution' Default "Testing/475-wait-one" $ \g -> do+ , testSolution' Default "Testing/475-wait-one" CheckForBadErrors $ \g -> do let t = g ^. ticks r1Waits = g ^?! robotMap . ix 1 . to waitingUntil active = IS.member 1 $ g ^. activeRobots waiting = elem 1 . concat . M.elems $ g ^. waitingRobots- assertBool "The game should only take one tick" $ t == 1+ assertBool "The game should only take two ticks" $ t == 2 assertBool "Robot 1 should have waiting machine" $ isJust r1Waits assertBool "Robot 1 should be still active" active assertBool "Robot 1 should not be in waiting set" $ not waiting@@ -186,13 +217,18 @@ , testSolution Default "Testing/504-teleport-self" , testSolution Default "Testing/508-capability-subset" , testGroup+ "Possession criteria (#858)"+ [ testSolution Default "Testing/858-inventory/858-possession-objective"+ , testSolution Default "Testing/858-inventory/858-counting-objective"+ , testSolution Default "Testing/858-inventory/858-nonpossession-objective"+ ]+ , testGroup "Require (#201)" [ testSolution Default "Testing/201-require/201-require-device" , testSolution Default "Testing/201-require/201-require-device-creative" , testSolution Default "Testing/201-require/201-require-device-creative1" , testSolution Default "Testing/201-require/201-require-entities"- , expectFailBecause "Awaiting fix (#540)" $- testSolution Default "Testing/201-require/201-require-entities-def"+ , testSolution Default "Testing/201-require/201-require-entities-def" , testSolution Default "Testing/201-require/533-reprogram-simple" , testSolution Default "Testing/201-require/533-reprogram" ]@@ -206,6 +242,21 @@ , testSolution Default "Testing/699-movement-fail/699-move-liquid" , testSolution Default "Testing/699-movement-fail/699-teleport-blocked" , testSolution Default "Testing/710-multi-robot"+ , testSolution Default "Testing/920-meet"+ , testSolution Default "Testing/955-heading"+ , testSolution' Default "Testing/397-wrong-missing" CheckForBadErrors $ \g -> do+ let msgs =+ (g ^. messageQueue . to seqToTexts)+ <> (g ^.. robotMap . traverse . robotLog . to seqToTexts . traverse)++ assertBool "Should be some messages" (not (null msgs))+ assertBool "Error messages should not mention treads" $+ not (any ("treads" `T.isInfixOf`) msgs)+ assertBool "Error message should mention GPS receiver" $+ any ("GPS receiver" `T.isInfixOf`) msgs+ , testSolution Default "Testing/961-custom-capabilities"+ , testSolution Default "Testing/956-GPS"+ , testSolution Default "Testing/958-isempty" ] ] where@@ -213,30 +264,38 @@ -- expectFailIf b = if b then expectFailBecause else (\_ x -> x) testSolution :: Time -> FilePath -> TestTree- testSolution s p = testSolution' s p (const $ pure ())+ testSolution s p = testSolution' s p CheckForBadErrors (const $ pure ()) - testSolution' :: Time -> FilePath -> (GameState -> Assertion) -> TestTree- testSolution' s p verify = testCase p $ do- Right gs <- runExceptT $ initGameStateForScenario p Nothing Nothing- case gs ^. winSolution of- Nothing -> assertFailure "No solution to test!"- Just sol -> do- let gs' = gs & baseRobot . machine .~ initMachine sol Ctx.empty emptyStore- m <- timeout (time s) (snd <$> runStateT playUntilWin gs')- case m of- Nothing -> assertFailure "Timed out - this likely means that the solution did not work."- Just g -> do- -- When debugging, try logging all robot messages.- -- printAllLogs- noBadErrors g- verify g+ testSolution' :: Time -> FilePath -> ShouldCheckBadErrors -> (GameState -> Assertion) -> TestTree+ testSolution' s p shouldCheckBadErrors verify = testCase p $ do+ out <- runExceptT $ initGameStateForScenario p Nothing Nothing+ case out of+ Left x -> assertFailure $ unwords ["Failure in initGameStateForScenario:", T.unpack x]+ Right gs -> case gs ^. winSolution of+ Nothing -> assertFailure "No solution to test!"+ Just sol@(ProcessedTerm _ _ reqCtx) -> do+ let gs' =+ gs+ -- See #827 for an explanation of why it's important to add to+ -- the robotContext defReqs here (and also why this will,+ -- hopefully, eventually, go away).+ & baseRobot . robotContext . defReqs <>~ reqCtx+ & baseRobot . machine .~ initMachine sol Ctx.empty emptyStore+ m <- timeout (time s) (snd <$> runStateT playUntilWin gs')+ case m of+ Nothing -> assertFailure "Timed out - this likely means that the solution did not work."+ Just g -> do+ -- When debugging, try logging all robot messages.+ -- printAllLogs+ when (shouldCheckBadErrors == CheckForBadErrors) $ noBadErrors g+ verify g playUntilWin :: StateT GameState IO () playUntilWin = do w <- use winCondition b <- gets badErrorsInLogs when (null b) $ case w of- Won _ -> return ()+ WinConditions (Won _) _ -> return () _ -> gameTick >> playUntilWin noBadErrors :: GameState -> Assertion@@ -251,8 +310,10 @@ (g ^. robotMap) <> filter isBad (seqToTexts $ g ^. messageQueue) where- seqToTexts = map (view leText) . toList isBad m = "Fatal error:" `T.isInfixOf` m || "swarm/issues" `T.isInfixOf` m++seqToTexts :: Seq LogEntry -> [Text]+seqToTexts = map (view leText) . toList printAllLogs :: GameState -> IO () printAllLogs g =
test/unit/Main.hs view
@@ -20,8 +20,10 @@ testProperty, (==>), )+import TestBoolExpr (testBoolExpr) import TestEval (testEval) import TestInventory (testInventory)+import TestLSP (testLSP) import TestLanguagePipeline (testLanguagePipeline) import TestModel (testModel) import TestNotification (testNotification)@@ -41,11 +43,13 @@ "Tests" [ testLanguagePipeline , testPrettyConst+ , testBoolExpr , testEval g , testModel , testInventory , testNotification g , testMisc+ , testLSP ] testMisc :: TestTree
+ test/unit/TestBoolExpr.hs view
@@ -0,0 +1,103 @@+{-# LANGUAGE OverloadedStrings #-}++-- | Boolean expression unit tests+module TestBoolExpr where++import Data.BoolExpr qualified as BE+import Data.BoolExpr.Simplify qualified as Simplify+import Data.List.NonEmpty (NonEmpty ((:|)))+import Data.Set qualified as Set+import Swarm.Game.Scenario.Objective.Logic+import Swarm.Game.Scenario.Objective.WinCheck qualified as WC+import Test.Tasty+import Test.Tasty.HUnit++testBoolExpr :: TestTree+testBoolExpr =+ testGroup+ "Boolean evaluation"+ [ testGroup+ "Expression simplification"+ [ testGroup+ "Return true if the expression can be simplified to False"+ [ testGroup+ "Effect of constant literals"+ [ testCase+ "False input via single literal"+ $ expectTrue BE.BFalse+ , testCase+ "True input via composed literals"+ $ expectFalse BE.BTrue+ , testCase+ "False input via composed literals"+ $ expectTrue+ $ BE.BOr BE.BFalse BE.BFalse+ , testCase+ "True input via composed literals"+ $ expectFalse+ $ BE.BOr BE.BFalse BE.BTrue+ , testCase+ "Constant OR'd with False"+ $ expectFalse+ $ BE.BOr BE.BFalse (BE.BConst (BE.Positive "foo"))+ , testCase+ "Constant OR'd with True"+ $ expectFalse+ $ BE.BOr (BE.BConst (BE.Positive "foo")) BE.BTrue+ , testCase+ "Constant AND'd with False"+ $ expectTrue+ $ BE.BAnd BE.BFalse (BE.BConst (BE.Positive "foo"))+ , testCase+ "Constant AND'd with True"+ $ expectFalse+ $ BE.BAnd (BE.BConst (BE.Positive "foo")) BE.BTrue+ , testCase+ "Nested Constants AND'd with False within OR"+ $ expectTrue+ $ BE.BOr+ (BE.BAnd BE.BFalse (BE.BConst (BE.Positive "foo")))+ (BE.BAnd (BE.BConst (BE.Positive "bar")) BE.BFalse)+ , testCase+ "Deeply nested Constants AND'd with False within OR with multiple negations"+ $ expectTrue+ $ BE.BOr+ (BE.BAnd (BE.BNot BE.BTrue) (BE.BNot (BE.BNot (BE.BNot (BE.BConst (BE.Positive "foo"))))))+ (BE.BAnd (BE.BConst (BE.Positive "bar")) (BE.BNot (BE.BNot BE.BFalse)))+ ]+ , testGroup+ "Effect of contradicting named constants"+ [ testCase+ "via NOT operator"+ $ expectTrue+ $ BE.BAnd (BE.BNot (BE.BConst (BE.Positive "foo"))) (BE.BConst (BE.Positive "foo"))+ , testCase+ "via signedness"+ $ expectTrue+ $ BE.BAnd (BE.BConst (BE.Positive "foo")) (BE.BConst (BE.Negative "foo"))+ ]+ ]+ ]+ , testGroup+ "Prerequisite expressions"+ [ testCase+ "A negated goal is completed"+ $ assertBool "Should have returned true"+ $ WC.isUnwinnablePrereq (Set.singleton "b") demoPrereqs+ , testCase+ "A non-negated goal is completed"+ $ assertBool "Should have returned false"+ $ not+ $ WC.isUnwinnablePrereq (Set.singleton "c") demoPrereqs+ ]+ ]+ where+ expectTrue, expectFalse :: BE.BoolExpr String -> Assertion+ expectTrue = assertBool "Should have returned true" . Simplify.cannotBeTrue+ expectFalse = assertBool "Should have returned false" . not . Simplify.cannotBeTrue++ demoPrereqs :: Prerequisite ObjectiveLabel+ demoPrereqs =+ And $+ Id "a"+ :| [Not (Id "b"), Id "c"]
test/unit/TestEval.hs view
@@ -5,10 +5,11 @@ module TestEval where import Control.Lens ((^.), _3)+import Data.Char (ord) import Data.Text (Text) import Data.Text qualified as T import Swarm.Game.State-import Swarm.Game.Value+import Swarm.Language.Value import Test.Tasty import Test.Tasty.HUnit import Test.Tasty.QuickCheck@@ -238,6 +239,27 @@ in T.concat ["(", t1, ",", t2, ") == split (chars ", t1, ") (", t1, " ++ ", t2, ")"] `evaluatesToP` VBool True )+ , testProperty+ "charAt"+ $ \i (NonEmpty s) ->+ let i' = i `mod` length s+ in T.concat ["charAt ", from @String (show i'), " ", tquote s]+ `evaluatesToP` VInt (fromIntegral (ord (s !! i')))+ , testCase+ "toChar 97"+ ("toChar 97 == \"a\"" `evaluatesTo` VBool True)+ , testProperty+ "chars/toChar"+ ( \(NonNegative (i :: Integer)) ->+ T.concat ["chars (toChar ", from @String (show i), ")"]+ `evaluatesToP` VInt 1+ )+ , testProperty+ "charAt/toChar"+ ( \(NonNegative i) ->+ T.concat ["charAt 0 (toChar ", from @String (show i), ")"]+ `evaluatesToP` VInt i+ ) ] ] where@@ -251,7 +273,7 @@ Left err -> p err @? "Expected predicate did not hold on error message "- ++ from @Text @String err+ ++ from @Text @String err evaluatesTo :: Text -> Value -> Assertion evaluatesTo tm val = do
+ test/unit/TestLSP.hs view
@@ -0,0 +1,88 @@+{-# LANGUAGE OverloadedStrings #-}++-- | LSP unit tests+module TestLSP (testLSP) where++import Data.Text (Text)+import Data.Text.IO qualified as TIO+import Swarm.Language.LSP.VarUsage qualified as VU+import Swarm.Language.Parse (readTerm')+import Swarm.Language.Syntax qualified as S+import System.FilePath ((</>))+import Test.Tasty+import Test.Tasty.HUnit++baseTestPath :: FilePath+baseTestPath = "data/test/language-snippets/warnings/unused-vars"++data UnusedVar = UnusedVar S.Var VU.BindingType+ deriving (Eq, Show)++simplifyWarning :: VU.VarUsage -> UnusedVar+simplifyWarning (VU.VarUsage (S.LV _ v) scope) = UnusedVar v scope++testLSP :: TestTree+testLSP =+ testGroup+ "Unused variable warnings"+ [ testCase "outer lambda" $+ checkFile "multiple-lambda-first-unused.sw" $+ pure $+ UnusedVar "x" VU.Lambda+ , testCase "inner lambda" $+ checkFile "multiple-lambda-second-unused.sw" $+ pure $+ UnusedVar "y" VU.Lambda+ , testCase "shadowed variable name" $+ checkFile "shadowed-variable-lambda-unused.sw" $+ pure $+ UnusedVar "x" VU.Lambda+ , testCase "outer let" $+ checkFile "multiple-let-first-unused.sw" $+ pure $+ UnusedVar "x" VU.Let+ , testCase "outer let" $+ checkFile "multiple-let-second-unused.sw" $+ pure $+ UnusedVar "y" VU.Let+ , testCase "multiple unused let" $+ checkFile+ "multiple-let-all-unused.sw"+ [UnusedVar "x" VU.Let, UnusedVar "y" VU.Let]+ , testCase "shadowed let without usage" $+ checkFile+ "shadowed-variable-let-unused.sw"+ [UnusedVar "x" VU.Let]+ , testCase "shadowed let with intermediate usage" $+ checkFile+ "shadowed-variable-let-intermediate-use.sw"+ []+ , testCase "recursive let" $+ checkFile+ "recursive-let.sw"+ [UnusedVar "fac" VU.Let]+ , testCase "single unused bind" $+ checkFile+ "single-bind-unused.sw"+ [UnusedVar "x" VU.Bind]+ , testCase "single used bind" $+ checkFile+ "single-bind-used.sw"+ []+ ]+ where+ checkFile :: FilePath -> [UnusedVar] -> IO ()+ checkFile filename expectedWarnings = do+ content <- TIO.readFile fullPath+ let actualWarnings = getWarnings content+ assertEqual "failed" expectedWarnings actualWarnings+ where+ fullPath = baseTestPath </> filename++ getWarnings :: Text -> [UnusedVar]+ getWarnings content =+ case readTerm' content of+ Right (Just term) -> map simplifyWarning problems+ where+ VU.Usage _ problems = VU.getUsage mempty term+ _ -> []
test/unit/TestLanguagePipeline.hs view
@@ -1,15 +1,24 @@ {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TemplateHaskell #-} -- | Swarm unit tests module TestLanguagePipeline where +import Control.Arrow ((&&&))+import Control.Lens (toListOf)+import Control.Lens.Plated (universe) import Data.Aeson (eitherDecode, encode) import Data.Either import Data.Maybe import Data.Text (Text) import Data.Text qualified as T import Data.Text.Encoding qualified as T-import Swarm.Language.Pipeline (processTerm)+import Swarm.Language.Module (Module (..))+import Swarm.Language.Parse.QQ (tyQ)+import Swarm.Language.Pipeline (ProcessedTerm (..), processTerm)+import Swarm.Language.Pipeline.QQ (tmQ)+import Swarm.Language.Syntax import Swarm.Language.Typecheck (isSimpleUType) import Swarm.Language.Types import Test.Tasty@@ -245,11 +254,38 @@ "void - valid type signature" (valid "def f : void -> a = \\x. undefined end") ]+ , testGroup+ "type annotations"+ [ testCase+ "annotate 1 + 1"+ ( assertEqual+ "type annotations"+ (toListOf traverse (getSyntax [tmQ| 1 + 1 |]))+ [[tyQ| int -> int -> int|], [tyQ|int|], [tyQ|int -> int|], [tyQ|int|], [tyQ|int|]]+ )+ , testCase+ "get all annotated variable types"+ ( let s =+ getSyntax+ [tmQ| def f : (int -> int) -> int -> text = \g. \x. format (g x) end |]++ isVar (TVar {}) = True+ isVar _ = False+ getVars = map (_sTerm &&& _sType) . filter (isVar . _sTerm) . universe+ in assertEqual+ "variable types"+ (getVars s)+ ( [ (TVar "g", [tyQ| int -> int |])+ , (TVar "x", [tyQ| int |])+ ]+ )+ )+ ] ] where valid = flip process "" - roundTrip txt = assertEqual "rountrip" term (decodeThrow $ encode term)+ roundTrip txt = assertEqual "roundtrip" term (decodeThrow $ encode term) where decodeThrow v = case eitherDecode v of Left e -> error $ "Decoding of " <> from (T.decodeUtf8 (from v)) <> " failed with: " <> from e@@ -264,3 +300,6 @@ Right _ | expect == "" -> pure () | otherwise -> error "Unexpected success"++ getSyntax :: ProcessedTerm -> Syntax' Polytype+ getSyntax (ProcessedTerm (Module s _) _ _) = s
test/unit/TestNotification.hs view
@@ -34,7 +34,7 @@ assertEqual "There should be two messages in queue" [0, 1] (view leTime <$> gs' ^. messageNotifications . notificationsContent) assertNew (gs' & lastSeenMessageTime .~ 0) 1 "message" messageNotifications , testCase "new message after log" $ do- gs' <- goodPlay "create \"logger\"; install self \"logger\"; log \"Hello world!\""+ gs' <- goodPlay "create \"logger\"; equip \"logger\"; log \"Hello world!\"" let r = gs' ^?! robotMap . ix (-1) assertBool "There should be one log entry in robots log" (length (r ^. robotLog) == 1) assertEqual "The hypothetical robot should be in focus" (Just (r ^. robotID)) (view robotID <$> focusedRobot gs')
test/unit/TestPretty.hs view
@@ -36,17 +36,22 @@ , testCase "operators #8 - double unary negation" ( equalPretty "-(-1)" $- TApp (TConst Neg) $ TApp (TConst Neg) (TInt 1)+ TApp (TConst Neg) $+ TApp (TConst Neg) (TInt 1) ) , testCase "operators #8 - unary negation with strongly fixing binary operator" ( equalPretty "-1 ^ (-2)" $- TApp (TConst Neg) $ mkOp' Exp (TInt 1) $ TApp (TConst Neg) (TInt 2)+ TApp (TConst Neg) $+ mkOp' Exp (TInt 1) $+ TApp (TConst Neg) (TInt 2) ) , testCase "operators #8 - unary negation with weakly fixing binary operator" ( equalPretty "-(1 + -2)" $- TApp (TConst Neg) $ mkOp' Add (TInt 1) $ TApp (TConst Neg) (TInt 2)+ TApp (TConst Neg) $+ mkOp' Add (TInt 1) $+ TApp (TConst Neg) (TInt 2) ) , testCase "operators #8 - simple infix operator"
test/unit/TestUtil.hs view
@@ -14,9 +14,9 @@ import Swarm.Game.Robot import Swarm.Game.State import Swarm.Game.Step (gameTick, hypotheticalRobot, stepCESK)-import Swarm.Game.Value import Swarm.Language.Context import Swarm.Language.Pipeline (ProcessedTerm (..), processTerm)+import Swarm.Language.Value eval :: GameState -> Text -> IO (GameState, Robot, Either Text (Value, Int)) eval g = either (return . (g,hypotheticalRobot undefined 0,) . Left) (evalPT g) . processTerm1