swarm 0.4 → 0.5.0.0
raw patch · 183 files changed
+6792/−2594 lines, 183 filesdep +astardep +palettedep +pandocdep −criteriondep ~brickdep ~megaparsecdep ~prettyprinterPVP ok
version bump matches the API change (PVP)
Dependencies added: astar, palette, pandoc, pandoc-types, scientific, tasty-bench, terminal-size
Dependencies removed: criterion
Dependency ranges changed: brick, megaparsec, prettyprinter, tasty
API changes (from Hackage documentation)
- Swarm.Doc.Gen: capabilityPage :: PageAddress -> EntityMap -> Text
- Swarm.Doc.Gen: commandsPage :: Text
- Swarm.Doc.Gen: editorList :: EditorType -> [Text] -> Text
- Swarm.Doc.Gen: noPageAddresses :: PageAddress
- Swarm.Game.Achievement.Attainment: achievementJsonOptions :: Options
- Swarm.Game.Achievement.Definitions: categorizedAchievementJsonOptions :: Options
- Swarm.Game.CESK: data Cell
- Swarm.Game.CESK: instance Data.Aeson.Types.FromJSON.FromJSON Swarm.Game.CESK.Cell
- Swarm.Game.CESK: instance Data.Aeson.Types.ToJSON.ToJSON Swarm.Game.CESK.Cell
- Swarm.Game.CESK: instance GHC.Classes.Eq Swarm.Game.CESK.Cell
- Swarm.Game.CESK: instance GHC.Generics.Generic Swarm.Game.CESK.Cell
- Swarm.Game.CESK: instance GHC.Show.Show Swarm.Game.CESK.Cell
- Swarm.Game.CESK: lookupCell :: Addr -> Store -> Maybe Cell
- Swarm.Game.CESK: setCell :: Addr -> Cell -> Store -> Store
- Swarm.Game.Log: Critical :: ErrorLevel
- Swarm.Game.Log: Debug :: ErrorLevel
- Swarm.Game.Log: Error :: ErrorLevel
- Swarm.Game.Log: ErrorTrace :: ErrorLevel -> LogSource
- Swarm.Game.Log: Located :: a -> LogLocation a
- Swarm.Game.Log: LogEntry :: TickNumber -> LogSource -> Text -> Int -> LogLocation (Cosmic Location) -> Text -> LogEntry
- Swarm.Game.Log: Logged :: LogSource
- Swarm.Game.Log: Omnipresent :: LogLocation a
- Swarm.Game.Log: Said :: LogSource
- Swarm.Game.Log: Warning :: ErrorLevel
- Swarm.Game.Log: [_leLocation] :: LogEntry -> LogLocation (Cosmic 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 -> TickNumber
- Swarm.Game.Log: data ErrorLevel
- Swarm.Game.Log: data LogEntry
- Swarm.Game.Log: data LogLocation a
- 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.FromJSON.FromJSON a => Data.Aeson.Types.FromJSON.FromJSON (Swarm.Game.Log.LogLocation a)
- 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 Data.Aeson.Types.ToJSON.ToJSON a => Data.Aeson.Types.ToJSON.ToJSON (Swarm.Game.Log.LogLocation a)
- 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.Eq a => GHC.Classes.Eq (Swarm.Game.Log.LogLocation a)
- 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.Classes.Ord a => GHC.Classes.Ord (Swarm.Game.Log.LogLocation a)
- Swarm.Game.Log: instance GHC.Generics.Generic (Swarm.Game.Log.LogLocation a)
- 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: instance GHC.Show.Show a => GHC.Show.Show (Swarm.Game.Log.LogLocation a)
- Swarm.Game.Log: leLocation :: Lens' LogEntry (LogLocation (Cosmic 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 TickNumber
- Swarm.Game.Recipe: [_recipeRequirements] :: Recipe e -> IngredientList e
- Swarm.Game.Recipe: recipeRequirements :: Lens' (Recipe e) (IngredientList e)
- Swarm.Game.Recipe: reqRecipeMap :: [Recipe Entity] -> IntMap [Recipe Entity]
- Swarm.Game.Robot: instance (Data.Aeson.Types.ToJSON.ToJSON (Swarm.Game.Robot.RobotLocation phase), Data.Aeson.Types.ToJSON.ToJSON (Swarm.Game.Robot.RobotID phase)) => Data.Aeson.Types.ToJSON.ToJSON (Swarm.Game.Robot.RobotR phase)
- Swarm.Game.Robot: tickSteps :: Lens' Robot Int
- Swarm.Game.Scenario.Topography.EntityFacade: type EntityName = Text
- Swarm.Game.State: [initAdjList] :: GameStateConfig -> Array Int Text
- Swarm.Game.State: [initNameList] :: GameStateConfig -> Array Int Text
- Swarm.Game.State: adjList :: Getter GameState (Array Int Text)
- Swarm.Game.State: nameList :: Getter GameState (Array Int Text)
- Swarm.Game.State: recipesReq :: Lens' GameState (IntMap [Recipe Entity])
- Swarm.Game.Step: MoveFailure :: RobotFailure -> RobotFailure -> MoveFailure
- Swarm.Game.Step: MoveFailureDetails :: Entity -> MoveFailureMode -> MoveFailureDetails
- Swarm.Game.Step: PathBlocked :: MoveFailureMode
- Swarm.Game.Step: PathLiquid :: MoveFailureMode
- Swarm.Game.Step: [failIfBlocked] :: MoveFailure -> RobotFailure
- Swarm.Game.Step: [failIfDrown] :: MoveFailure -> RobotFailure
- Swarm.Game.Step: cmdExn :: Const -> [Text] -> Exn
- Swarm.Game.Step: data MoveFailure
- Swarm.Game.Step: data MoveFailureDetails
- Swarm.Game.Step: data MoveFailureMode
- Swarm.Game.Step: entityAt :: Has (State GameState) sig m => Cosmic Location -> m (Maybe Entity)
- Swarm.Game.Step: flagRedraw :: Has (State GameState) sig m => m ()
- Swarm.Game.Step: getNow :: Has (Lift IO) sig m => m TimeSpec
- Swarm.Game.Step: hasCapability :: (Has (State Robot) sig m, Has (State GameState) sig m) => Capability -> m Bool
- Swarm.Game.Step: hasCapabilityFor :: (Has (State Robot) sig m, Has (State GameState) sig m, Has (Throw Exn) sig m) => Capability -> Term -> m ()
- Swarm.Game.Step: isPrivilegedBot :: (Has (State GameState) sig m, Has (State Robot) sig m) => m Bool
- Swarm.Game.Step: randomName :: Has (State GameState) sig m => m Text
- Swarm.Game.Step: robotWithID :: Has (State GameState) sig m => RID -> m (Maybe Robot)
- Swarm.Game.Step: robotWithName :: Has (State GameState) sig m => Text -> m (Maybe Robot)
- Swarm.Game.Step: type HasRobotStepState sig m = (Has (State GameState) sig m, Has (State Robot) sig m, Has (Throw Exn) sig m)
- Swarm.Game.Step: uniform :: (Has (State GameState) sig m, UniformRange a) => (a, a) -> m a
- Swarm.Game.Step: updateEntityAt :: Has (State GameState) sig m => Cosmic Location -> (Maybe Entity -> Maybe Entity) -> m ()
- Swarm.Game.Step: weightedChoice :: Has (State GameState) sig m => (a -> Integer) -> [a] -> m (Maybe a)
- Swarm.Game.Step: zoomWorld :: Has (State GameState) sig m => SubworldName -> StateC (World Int Entity) Identity b -> m (Maybe b)
- Swarm.Game.World.Parse: sepByNE :: MonadPlus m => m a -> m sep -> m (NonEmpty a)
- Swarm.Language.Pretty: instance Swarm.Language.Pretty.PrettyPrec t => Swarm.Language.Pretty.PrettyPrec (Swarm.Language.Types.TypeF t)
- Swarm.Language.Syntax: erase :: Term' ty -> Term
- Swarm.TUI.Attr: blueAttr :: AttrName
- Swarm.TUI.Attr: boldAttr :: AttrName
- Swarm.TUI.Attr: customEditFocusedAttr :: AttrName
- Swarm.TUI.Attr: cyanAttr :: AttrName
- Swarm.TUI.Attr: defAttr :: AttrName
- Swarm.TUI.Attr: dimAttr :: AttrName
- Swarm.TUI.Attr: dirtAttr :: AttrName
- Swarm.TUI.Attr: entityAttr :: AttrName
- Swarm.TUI.Attr: grassAttr :: AttrName
- Swarm.TUI.Attr: greenAttr :: AttrName
- Swarm.TUI.Attr: highlightAttr :: AttrName
- Swarm.TUI.Attr: iceAttr :: AttrName
- Swarm.TUI.Attr: infoAttr :: AttrName
- Swarm.TUI.Attr: italicAttr :: AttrName
- Swarm.TUI.Attr: lightCyanAttr :: AttrName
- Swarm.TUI.Attr: magentaAttr :: AttrName
- Swarm.TUI.Attr: notifAttr :: AttrName
- Swarm.TUI.Attr: plantAttr :: AttrName
- Swarm.TUI.Attr: redAttr :: AttrName
- Swarm.TUI.Attr: robotAttr :: AttrName
- Swarm.TUI.Attr: rockAttr :: AttrName
- Swarm.TUI.Attr: stoneAttr :: AttrName
- Swarm.TUI.Attr: swarmAttrMap :: AttrMap
- Swarm.TUI.Attr: toAttrName :: Attribute -> AttrName
- Swarm.TUI.Attr: waterAttr :: AttrName
- Swarm.TUI.Attr: worldAttributes :: [(AttrName, Attr)]
- Swarm.TUI.Attr: worldPrefix :: AttrName
- Swarm.TUI.Attr: yellowAttr :: AttrName
- Swarm.TUI.Editor.Util: getTerrainAt :: WorldEditor Name -> MultiWorld Int Entity -> Cosmic Coords -> TerrainType
- Swarm.TUI.Model: stdAdjList :: Lens' RuntimeState (Array Int Text)
- Swarm.TUI.Model: stdNameList :: Lens' RuntimeState (Array Int Text)
- Swarm.TUI.Model.UI: [_uiError] :: UIState -> Maybe Text
- Swarm.TUI.Model.UI: [_uiMoreInfoBot] :: UIState -> Bool
- Swarm.TUI.Model.UI: [_uiMoreInfoTop] :: UIState -> Bool
- Swarm.TUI.Model.UI: uiError :: Lens' UIState (Maybe Text)
- Swarm.TUI.Model.UI: uiMoreInfoBot :: Lens' UIState Bool
- Swarm.TUI.Model.UI: uiMoreInfoTop :: Lens' UIState Bool
- Swarm.TUI.View: drawWorld :: UIState -> GameState -> 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: 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.Web: RobotID :: Int -> RobotID
- Swarm.Web: WebStartError :: String -> WebStartResult
- Swarm.Web: WebStarted :: WebStartResult
- Swarm.Web: api :: Proxy ToplevelAPI
- Swarm.Web: data WebStartResult
- Swarm.Web: docsBS :: ByteString
- Swarm.Web: mkApp :: ReadableIORef AppState -> BChan AppEvent -> Server SwarmAPI
- Swarm.Web: newtype RobotID
- Swarm.Web: swarmApi :: Proxy SwarmAPI
- Swarm.Web: type ToplevelAPI = SwarmAPI :<|> Raw
+ Swarm.Doc.Gen: Scenario :: SheetType
+ Swarm.Doc.Gen: Vim :: EditorType
+ Swarm.Doc.Gen: [WebAPIEndpoints] :: GenerateDocs
+ Swarm.Doc.Gen: loadStandaloneScenario :: (Has (Throw SystemFailure) sig m, Has (Lift IO) sig m) => FilePath -> m (Scenario, (WorldMap, EntityMap, [Recipe Entity]))
+ Swarm.Doc.Schema.Arrangement: sortAndPruneSchemas :: SchemaIdReference -> [SchemaData] -> [SchemaData]
+ Swarm.Doc.Schema.Parse: ListMembers :: ItemDescription SwarmSchema -> Members
+ Swarm.Doc.Schema.Parse: ObjectProperties :: Map Text SwarmSchema -> Members
+ Swarm.Doc.Schema.Parse: SchemaData :: FilePath -> ToplevelSchema -> [Pandoc] -> SchemaData
+ Swarm.Doc.Schema.Parse: ToplevelSchema :: Text -> Maybe Pandoc -> SwarmSchema -> Maybe Members -> [FilePath] -> ToplevelSchema
+ Swarm.Doc.Schema.Parse: [content] :: ToplevelSchema -> SwarmSchema
+ Swarm.Doc.Schema.Parse: [description] :: ToplevelSchema -> Maybe Pandoc
+ Swarm.Doc.Schema.Parse: [footerPaths] :: ToplevelSchema -> [FilePath]
+ Swarm.Doc.Schema.Parse: [markdownFooters] :: SchemaData -> [Pandoc]
+ Swarm.Doc.Schema.Parse: [members] :: ToplevelSchema -> Maybe Members
+ Swarm.Doc.Schema.Parse: [schemaContent] :: SchemaData -> ToplevelSchema
+ Swarm.Doc.Schema.Parse: [schemaPath] :: SchemaData -> FilePath
+ Swarm.Doc.Schema.Parse: [title] :: ToplevelSchema -> Text
+ Swarm.Doc.Schema.Parse: data Members
+ Swarm.Doc.Schema.Parse: data SchemaData
+ Swarm.Doc.Schema.Parse: data ToplevelSchema
+ Swarm.Doc.Schema.Parse: instance Data.Aeson.Types.FromJSON.FromJSON Swarm.Doc.Schema.Parse.ToplevelSchema
+ Swarm.Doc.Schema.Parse: instance GHC.Classes.Eq Swarm.Doc.Schema.Parse.Members
+ Swarm.Doc.Schema.Parse: instance GHC.Classes.Eq Swarm.Doc.Schema.Parse.ToplevelSchema
+ Swarm.Doc.Schema.Parse: instance GHC.Classes.Ord Swarm.Doc.Schema.Parse.Members
+ Swarm.Doc.Schema.Parse: instance GHC.Classes.Ord Swarm.Doc.Schema.Parse.ToplevelSchema
+ Swarm.Doc.Schema.Parse: instance GHC.Show.Show Swarm.Doc.Schema.Parse.Members
+ Swarm.Doc.Schema.Parse: instance GHC.Show.Show Swarm.Doc.Schema.Parse.ToplevelSchema
+ Swarm.Doc.Schema.Refined: ItemList :: [a] -> ItemDescription a
+ Swarm.Doc.Schema.Refined: ItemType :: a -> ItemDescription a
+ Swarm.Doc.Schema.Refined: SchemaRaw :: Maybe Text -> Maybe Value -> Maybe Text -> Maybe (SingleOrList Text) -> Maybe Text -> Maybe (Map Text SwarmSchema) -> Maybe (ItemDescription SwarmSchema) -> Maybe [Value] -> Maybe Text -> Maybe [SchemaRaw] -> Maybe [FilePath] -> Maybe Bool -> SchemaRaw
+ Swarm.Doc.Schema.Refined: SwarmSchema :: SchemaType -> Maybe Value -> Maybe Pandoc -> Maybe (Map Text SwarmSchema) -> Maybe (ItemDescription SwarmSchema) -> [Value] -> SwarmSchema
+ Swarm.Doc.Schema.Refined: [_Sref] :: SchemaRaw -> Maybe Text
+ Swarm.Doc.Schema.Refined: [_additionalProperties] :: SchemaRaw -> Maybe Bool
+ Swarm.Doc.Schema.Refined: [_default] :: SchemaRaw -> Maybe Value
+ Swarm.Doc.Schema.Refined: [_description] :: SchemaRaw -> Maybe Text
+ Swarm.Doc.Schema.Refined: [_examples] :: SchemaRaw -> Maybe [Value]
+ Swarm.Doc.Schema.Refined: [_footers] :: SchemaRaw -> Maybe [FilePath]
+ Swarm.Doc.Schema.Refined: [_items] :: SchemaRaw -> Maybe (ItemDescription SwarmSchema)
+ Swarm.Doc.Schema.Refined: [_name] :: SchemaRaw -> Maybe Text
+ Swarm.Doc.Schema.Refined: [_oneOf] :: SchemaRaw -> Maybe [SchemaRaw]
+ Swarm.Doc.Schema.Refined: [_properties] :: SchemaRaw -> Maybe (Map Text SwarmSchema)
+ Swarm.Doc.Schema.Refined: [_title] :: SchemaRaw -> Maybe Text
+ Swarm.Doc.Schema.Refined: [_type] :: SchemaRaw -> Maybe (SingleOrList Text)
+ Swarm.Doc.Schema.Refined: [defaultValue] :: SwarmSchema -> Maybe Value
+ Swarm.Doc.Schema.Refined: [examples] :: SwarmSchema -> [Value]
+ Swarm.Doc.Schema.Refined: [itemsDescription] :: SwarmSchema -> Maybe (ItemDescription SwarmSchema)
+ Swarm.Doc.Schema.Refined: [objectDescription] :: SwarmSchema -> Maybe Pandoc
+ Swarm.Doc.Schema.Refined: [properties] :: SwarmSchema -> Maybe (Map Text SwarmSchema)
+ Swarm.Doc.Schema.Refined: [schemaType] :: SwarmSchema -> SchemaType
+ Swarm.Doc.Schema.Refined: data ItemDescription a
+ Swarm.Doc.Schema.Refined: data SchemaRaw
+ Swarm.Doc.Schema.Refined: data SwarmSchema
+ Swarm.Doc.Schema.Refined: extractReferences :: SwarmSchema -> Set SchemaIdReference
+ Swarm.Doc.Schema.Refined: extractSchemaType :: SchemaRaw -> Maybe SchemaType
+ Swarm.Doc.Schema.Refined: getMarkdown :: MonadFail m => Text -> m Pandoc
+ Swarm.Doc.Schema.Refined: getSchemaReferences :: SchemaType -> [SchemaIdReference]
+ Swarm.Doc.Schema.Refined: instance Data.Aeson.Types.FromJSON.FromJSON Swarm.Doc.Schema.Refined.SchemaRaw
+ Swarm.Doc.Schema.Refined: instance Data.Aeson.Types.FromJSON.FromJSON Swarm.Doc.Schema.Refined.SwarmSchema
+ Swarm.Doc.Schema.Refined: instance Data.Aeson.Types.FromJSON.FromJSON a => Data.Aeson.Types.FromJSON.FromJSON (Swarm.Doc.Schema.Refined.ItemDescription a)
+ Swarm.Doc.Schema.Refined: instance GHC.Classes.Eq Swarm.Doc.Schema.Refined.SchemaRaw
+ Swarm.Doc.Schema.Refined: instance GHC.Classes.Eq Swarm.Doc.Schema.Refined.SwarmSchema
+ Swarm.Doc.Schema.Refined: instance GHC.Classes.Eq a => GHC.Classes.Eq (Swarm.Doc.Schema.Refined.ItemDescription a)
+ Swarm.Doc.Schema.Refined: instance GHC.Classes.Ord Swarm.Doc.Schema.Refined.SchemaRaw
+ Swarm.Doc.Schema.Refined: instance GHC.Classes.Ord Swarm.Doc.Schema.Refined.SwarmSchema
+ Swarm.Doc.Schema.Refined: instance GHC.Classes.Ord a => GHC.Classes.Ord (Swarm.Doc.Schema.Refined.ItemDescription a)
+ Swarm.Doc.Schema.Refined: instance GHC.Generics.Generic Swarm.Doc.Schema.Refined.SchemaRaw
+ Swarm.Doc.Schema.Refined: instance GHC.Show.Show Swarm.Doc.Schema.Refined.SchemaRaw
+ Swarm.Doc.Schema.Refined: instance GHC.Show.Show Swarm.Doc.Schema.Refined.SwarmSchema
+ Swarm.Doc.Schema.Refined: instance GHC.Show.Show a => GHC.Show.Show (Swarm.Doc.Schema.Refined.ItemDescription a)
+ Swarm.Doc.Schema.Refined: schemaJsonOptions :: Options
+ Swarm.Doc.Schema.Refined: toSwarmSchema :: MonadFail m => SchemaRaw -> m SwarmSchema
+ Swarm.Doc.Schema.Render: docFragmentsDir :: FilePath
+ Swarm.Doc.Schema.Render: formatNumberCompact :: Scientific -> String
+ Swarm.Doc.Schema.Render: fragmentHref :: Map SchemaIdReference Text -> SchemaIdReference -> Text
+ Swarm.Doc.Schema.Render: genMarkdown :: [SchemaData] -> Either Text Text
+ Swarm.Doc.Schema.Render: genPropsRow :: Bool -> Map SchemaIdReference Text -> (Text, SwarmSchema) -> [Blocks]
+ Swarm.Doc.Schema.Render: genScenarioSchemaDocs :: IO ()
+ Swarm.Doc.Schema.Render: listColumnHeadings :: [Text]
+ Swarm.Doc.Schema.Render: listToText :: Map SchemaIdReference Text -> SchemaType -> Inlines
+ Swarm.Doc.Schema.Render: loadFooterContent :: (FilePath, ToplevelSchema) -> IO SchemaData
+ Swarm.Doc.Schema.Render: makePandocTable :: Map SchemaIdReference Text -> SchemaData -> Pandoc
+ Swarm.Doc.Schema.Render: makePropsTable :: Bool -> [Text] -> Map SchemaIdReference Text -> Map Text SwarmSchema -> Blocks
+ Swarm.Doc.Schema.Render: makeTitleMap :: [SchemaData] -> Map SchemaIdReference Text
+ Swarm.Doc.Schema.Render: parseSchemaFile :: FileStemAndExtension -> IO (Either Text ToplevelSchema)
+ Swarm.Doc.Schema.Render: propertyColumnHeadings :: [Text]
+ Swarm.Doc.Schema.Render: recombineExtension :: FileStemAndExtension -> FilePath
+ Swarm.Doc.Schema.Render: renderValue :: Value -> Text
+ Swarm.Doc.Schema.Render: scenariosDir :: FilePath
+ Swarm.Doc.Schema.Render: schemaExtension :: String
+ Swarm.Doc.Schema.Render: schemasDir :: FilePath
+ Swarm.Doc.Schema.Render: type FileStemAndExtension = (FilePath, String)
+ Swarm.Doc.Schema.SchemaType: Alternatives :: [SchemaType] -> SchemaType
+ Swarm.Doc.Schema.SchemaType: ListOf :: SchemaType -> SchemaType
+ Swarm.Doc.Schema.SchemaType: Reference :: SchemaIdReference -> SchemaType
+ Swarm.Doc.Schema.SchemaType: SchemaIdReference :: Text -> SchemaIdReference
+ Swarm.Doc.Schema.SchemaType: Simple :: SingleOrList Text -> SchemaType
+ Swarm.Doc.Schema.SchemaType: SingleOrList :: [a] -> SingleOrList a
+ Swarm.Doc.Schema.SchemaType: [getList] :: SingleOrList a -> [a]
+ Swarm.Doc.Schema.SchemaType: data SchemaType
+ Swarm.Doc.Schema.SchemaType: fromFilePath :: FilePath -> SchemaIdReference
+ Swarm.Doc.Schema.SchemaType: instance Data.Aeson.Types.FromJSON.FromJSON a => Data.Aeson.Types.FromJSON.FromJSON (Swarm.Doc.Schema.SchemaType.SingleOrList a)
+ Swarm.Doc.Schema.SchemaType: instance GHC.Classes.Eq Swarm.Doc.Schema.SchemaType.SchemaIdReference
+ Swarm.Doc.Schema.SchemaType: instance GHC.Classes.Eq Swarm.Doc.Schema.SchemaType.SchemaType
+ Swarm.Doc.Schema.SchemaType: instance GHC.Classes.Eq a => GHC.Classes.Eq (Swarm.Doc.Schema.SchemaType.SingleOrList a)
+ Swarm.Doc.Schema.SchemaType: instance GHC.Classes.Ord Swarm.Doc.Schema.SchemaType.SchemaIdReference
+ Swarm.Doc.Schema.SchemaType: instance GHC.Classes.Ord Swarm.Doc.Schema.SchemaType.SchemaType
+ Swarm.Doc.Schema.SchemaType: instance GHC.Classes.Ord a => GHC.Classes.Ord (Swarm.Doc.Schema.SchemaType.SingleOrList a)
+ Swarm.Doc.Schema.SchemaType: instance GHC.Show.Show Swarm.Doc.Schema.SchemaType.SchemaIdReference
+ Swarm.Doc.Schema.SchemaType: instance GHC.Show.Show Swarm.Doc.Schema.SchemaType.SchemaType
+ Swarm.Doc.Schema.SchemaType: instance GHC.Show.Show a => GHC.Show.Show (Swarm.Doc.Schema.SchemaType.SingleOrList a)
+ Swarm.Doc.Schema.SchemaType: newtype SchemaIdReference
+ Swarm.Doc.Schema.SchemaType: newtype SingleOrList a
+ Swarm.Doc.Util: addLink :: Text -> Text -> Text
+ Swarm.Doc.Util: codeQuote :: Text -> Text
+ Swarm.Doc.Util: tshow :: Show a => a -> Text
+ Swarm.Doc.Util: wrap :: Char -> Text -> Text
+ Swarm.Game.Achievement.Definitions: SwapSame :: GameplayAchievement
+ Swarm.Game.CESK: data MemCell
+ Swarm.Game.CESK: instance Data.Aeson.Types.FromJSON.FromJSON Swarm.Game.CESK.MemCell
+ Swarm.Game.CESK: instance Data.Aeson.Types.ToJSON.ToJSON Swarm.Game.CESK.MemCell
+ Swarm.Game.CESK: instance GHC.Classes.Eq Swarm.Game.CESK.MemCell
+ Swarm.Game.CESK: instance GHC.Generics.Generic Swarm.Game.CESK.MemCell
+ Swarm.Game.CESK: instance GHC.Show.Show Swarm.Game.CESK.MemCell
+ Swarm.Game.CESK: instance Swarm.Util.WindowedCounter.Offsettable Swarm.Game.CESK.TickNumber
+ Swarm.Game.CESK: lookupStore :: Addr -> Store -> Maybe MemCell
+ Swarm.Game.CESK: setStore :: Addr -> MemCell -> Store -> Store
+ Swarm.Game.Entity: Combustibility :: Double -> (Integer, Integer) -> Maybe EntityName -> Combustibility
+ Swarm.Game.Entity: Combustible :: EntityProperty
+ Swarm.Game.Entity: [duration] :: Combustibility -> (Integer, Integer)
+ Swarm.Game.Entity: [ignition] :: Combustibility -> Double
+ Swarm.Game.Entity: [product] :: Combustibility -> Maybe EntityName
+ Swarm.Game.Entity: data Combustibility
+ Swarm.Game.Entity: defaultCombustibility :: Combustibility
+ Swarm.Game.Entity: entityCombustion :: Lens' Entity (Maybe Combustibility)
+ Swarm.Game.Entity: instance Data.Aeson.Types.FromJSON.FromJSON Swarm.Game.Entity.Combustibility
+ Swarm.Game.Entity: instance Data.Aeson.Types.ToJSON.ToJSON Swarm.Game.Entity.Combustibility
+ Swarm.Game.Entity: instance Data.Hashable.Class.Hashable Swarm.Game.Entity.Combustibility
+ Swarm.Game.Entity: instance GHC.Classes.Eq Swarm.Game.Entity.Combustibility
+ Swarm.Game.Entity: instance GHC.Classes.Ord Swarm.Game.Entity.Combustibility
+ Swarm.Game.Entity: instance GHC.Generics.Generic Swarm.Game.Entity.Combustibility
+ Swarm.Game.Entity: instance GHC.Read.Read Swarm.Game.Entity.Combustibility
+ Swarm.Game.Entity: instance GHC.Show.Show Swarm.Game.Entity.Combustibility
+ Swarm.Game.Entity: type EntityName = Text
+ Swarm.Game.Location: toAbsDirection :: Heading -> Maybe AbsoluteDir
+ Swarm.Game.Recipe: [_recipeCatalysts] :: Recipe e -> IngredientList e
+ Swarm.Game.Recipe: catRecipeMap :: [Recipe Entity] -> IntMap [Recipe Entity]
+ Swarm.Game.Recipe: recipeCatalysts :: Lens' (Recipe e) (IngredientList e)
+ Swarm.Game.ResourceLoading: NameGenerator :: Array Int Text -> Array Int Text -> NameGenerator
+ Swarm.Game.ResourceLoading: [adjList] :: NameGenerator -> Array Int Text
+ Swarm.Game.ResourceLoading: [nameList] :: NameGenerator -> Array Int Text
+ Swarm.Game.ResourceLoading: data NameGenerator
+ Swarm.Game.ResourceLoading: initNameGenerator :: Has (Throw SystemFailure) sig m => Map Text Text -> m NameGenerator
+ Swarm.Game.Robot: activityCounts :: Lens' Robot ActivityCounts
+ Swarm.Game.Robot: activityWindow :: Lens' ActivityCounts (WindowedCounter TickNumber)
+ Swarm.Game.Robot: commandsHistogram :: Lens' ActivityCounts (Map Const Int)
+ Swarm.Game.Robot: instance Data.Aeson.Types.FromJSON.FromJSON Swarm.Game.Robot.ActivityCounts
+ Swarm.Game.Robot: instance Data.Aeson.Types.ToJSON.ToJSON Swarm.Game.Robot.ActivityCounts
+ Swarm.Game.Robot: instance Data.Aeson.Types.ToJSON.ToJSON Swarm.Game.Robot.Robot
+ Swarm.Game.Robot: instance GHC.Classes.Eq Swarm.Game.Robot.ActivityCounts
+ Swarm.Game.Robot: instance GHC.Generics.Generic Swarm.Game.Robot.ActivityCounts
+ Swarm.Game.Robot: instance GHC.Show.Show Swarm.Game.Robot.ActivityCounts
+ Swarm.Game.Robot: lifetimeStepCount :: Lens' ActivityCounts Int
+ Swarm.Game.Robot: tangibleCommandCount :: Lens' ActivityCounts Int
+ Swarm.Game.Robot: tickStepBudget :: Lens' ActivityCounts Int
+ Swarm.Game.Robot: unwalkableEntities :: Lens' Robot (Set EntityName)
+ Swarm.Game.Scenario: loadStandaloneScenario :: (Has (Throw SystemFailure) sig m, Has (Lift IO) sig m) => FilePath -> m (Scenario, (WorldMap, EntityMap, [Recipe Entity]))
+ Swarm.Game.Scenario.Status: emptyLaunchParams :: Applicative f => ParameterizableLaunchParams a f
+ Swarm.Game.Scenario.Topography.Navigation.Util: orientationBasedRelativePosition :: Robot -> Cosmic Location -> Maybe (V2 Int32)
+ Swarm.Game.State: [initNameParts] :: GameStateConfig -> NameGenerator
+ Swarm.Game.State: buildWorldTuples :: Scenario -> NonEmpty SubworldDescription
+ Swarm.Game.State: data Discovery
+ Swarm.Game.State: data GameControls
+ Swarm.Game.State: data Landscape
+ Swarm.Game.State: data Messages
+ Swarm.Game.State: data Recipes
+ Swarm.Game.State: data RobotNaming
+ Swarm.Game.State: data TemporalState
+ Swarm.Game.State: discovery :: Lens' GameState Discovery
+ Swarm.Game.State: gameControls :: Lens' GameState GameControls
+ Swarm.Game.State: genMultiWorld :: NonEmpty SubworldDescription -> Seed -> MultiWorld Int Entity
+ Swarm.Game.State: genRobotTemplates :: Scenario -> NonEmpty (a, ([(Int, TRobot)], b)) -> [TRobot]
+ Swarm.Game.State: getRadioRange :: Maybe Robot -> Maybe Robot -> (Double, Double)
+ Swarm.Game.State: landscape :: Lens' GameState Landscape
+ Swarm.Game.State: messageInfo :: Lens' GameState Messages
+ Swarm.Game.State: nameGenerator :: Getter RobotNaming NameGenerator
+ Swarm.Game.State: recipesCat :: Lens' Recipes (IntMap [Recipe Entity])
+ Swarm.Game.State: recipesInfo :: Lens' GameState Recipes
+ Swarm.Game.State: robotNaming :: Lens' GameState RobotNaming
+ Swarm.Game.State: temporal :: Lens' GameState TemporalState
+ Swarm.Game.Step: type MoveFailureHandler = MoveFailureMode -> RobotFailure
+ Swarm.Game.Step.Combustion: addCombustionBot :: Has (State GameState) sig m => Entity -> Combustibility -> TimeSpec -> Cosmic Location -> m Integer
+ Swarm.Game.Step.Combustion: addIgnitionBot :: Has (State GameState) sig m => Integer -> Entity -> TimeSpec -> Cosmic Location -> m ()
+ Swarm.Game.Step.Combustion: combustionProgram :: Integer -> Combustibility -> ProcessedTerm
+ Swarm.Game.Step.Combustion: igniteCommand :: (HasRobotStepState sig m, Has (Lift IO) sig m) => Const -> Direction -> m ()
+ Swarm.Game.Step.Combustion: igniteNeighbor :: Has (State GameState) sig m => TimeSpec -> Integer -> Cosmic Location -> m ()
+ Swarm.Game.Step.Combustion: ignitionProgram :: Integer -> ProcessedTerm
+ Swarm.Game.Step.Pathfinding: EntityTarget :: EntityName -> PathfindingTarget
+ Swarm.Game.Step.Pathfinding: LocationTarget :: Location -> PathfindingTarget
+ Swarm.Game.Step.Pathfinding: data PathfindingTarget
+ Swarm.Game.Step.Pathfinding: pathCommand :: (HasRobotStepState sig m, Has (State GameState) sig m) => Maybe Integer -> Cosmic Location -> PathfindingTarget -> m (Maybe Direction)
+ Swarm.Game.Step.Util: MoveFailureDetails :: Entity -> MoveFailureMode -> MoveFailureDetails
+ Swarm.Game.Step.Util: PathBlocked :: MoveFailureMode
+ Swarm.Game.Step.Util: PathLiquid :: MoveFailureMode
+ Swarm.Game.Step.Util: checkMoveFailure :: HasRobotStepState sig m => Cosmic Location -> m (Maybe MoveFailureDetails)
+ Swarm.Game.Step.Util: checkMoveFailureUnprivileged :: HasRobotStepState sig m => Cosmic Location -> m (Maybe MoveFailureDetails)
+ Swarm.Game.Step.Util: cmdExn :: Const -> [Text] -> Exn
+ Swarm.Game.Step.Util: data MoveFailureDetails
+ Swarm.Game.Step.Util: data MoveFailureMode
+ Swarm.Game.Step.Util: deriveHeading :: HasRobotStepState sig m => Direction -> m Heading
+ Swarm.Game.Step.Util: entityAt :: Has (State GameState) sig m => Cosmic Location -> m (Maybe Entity)
+ Swarm.Game.Step.Util: flagRedraw :: Has (State GameState) sig m => m ()
+ Swarm.Game.Step.Util: getNeighborLocs :: Cosmic Location -> [Cosmic Location]
+ Swarm.Game.Step.Util: getNow :: Has (Lift IO) sig m => m TimeSpec
+ Swarm.Game.Step.Util: hasCapability :: (Has (State Robot) sig m, Has (State GameState) sig m) => Capability -> m Bool
+ Swarm.Game.Step.Util: hasCapabilityFor :: (Has (State Robot) sig m, Has (State GameState) sig m, Has (Throw Exn) sig m) => Capability -> Term -> m ()
+ Swarm.Game.Step.Util: holdsOrFail' :: Has (Throw Exn) sig m => Const -> Bool -> [Text] -> m ()
+ Swarm.Game.Step.Util: isJustOrFail' :: Has (Throw Exn) sig m => Const -> Maybe a -> [Text] -> m a
+ Swarm.Game.Step.Util: isPrivilegedBot :: (Has (State GameState) sig m, Has (State Robot) sig m) => m Bool
+ Swarm.Game.Step.Util: lookInDirection :: HasRobotStepState sig m => Direction -> m (Cosmic Location, Maybe Entity)
+ Swarm.Game.Step.Util: randomName :: Has (State GameState) sig m => m Text
+ Swarm.Game.Step.Util: robotWithID :: Has (State GameState) sig m => RID -> m (Maybe Robot)
+ Swarm.Game.Step.Util: robotWithName :: Has (State GameState) sig m => Text -> m (Maybe Robot)
+ Swarm.Game.Step.Util: type HasRobotStepState sig m = (Has (State GameState) sig m, Has (State Robot) sig m, Has (Throw Exn) sig m)
+ Swarm.Game.Step.Util: uniform :: (Has (State GameState) sig m, UniformRange a) => (a, a) -> m a
+ Swarm.Game.Step.Util: updateEntityAt :: Has (State GameState) sig m => Cosmic Location -> (Maybe Entity -> Maybe Entity) -> m ()
+ Swarm.Game.Step.Util: weightedChoice :: Has (State GameState) sig m => (a -> Integer) -> [a] -> m (Maybe a)
+ Swarm.Game.Step.Util: zoomWorld :: Has (State GameState) sig m => SubworldName -> StateC (World Int Entity) Identity b -> m (Maybe b)
+ Swarm.Game.Universe: getFiniteDistance :: DistanceMeasure b -> Maybe b
+ Swarm.Game.Value: instance Swarm.Game.Value.Valuable Swarm.Language.Direction.Direction
+ Swarm.Game.World.Modify: Add :: e -> CellModification e
+ Swarm.Game.World.Modify: Modified :: CellModification e -> CellUpdate e
+ Swarm.Game.World.Modify: NoChange :: Maybe e -> CellUpdate e
+ Swarm.Game.World.Modify: Remove :: e -> CellModification e
+ Swarm.Game.World.Modify: Swap :: e -> e -> CellModification e
+ Swarm.Game.World.Modify: classifyModification :: Maybe Entity -> Maybe Entity -> CellUpdate Entity
+ Swarm.Game.World.Modify: data CellModification e
+ Swarm.Game.World.Modify: data CellUpdate e
+ Swarm.Game.World.Modify: getModification :: CellUpdate e -> Maybe (CellModification e)
+ Swarm.Game.World.Render: getDisplayChar :: PCell EntityFacade -> Char
+ Swarm.Game.World.Render: getDisplayGrid :: Scenario -> GameState -> [[PCell EntityFacade]]
+ Swarm.Game.World.Render: printScenarioMap :: [String] -> IO ()
+ Swarm.Game.World.Render: renderScenarioMap :: FilePath -> IO [String]
+ Swarm.Language.Capability: CIgnite :: Capability
+ Swarm.Language.Capability: CPath :: Capability
+ Swarm.Language.Capability: CPing :: Capability
+ Swarm.Language.Capability: CWaypoint :: Capability
+ Swarm.Language.Pretty: Prec :: Int -> a -> Prec a
+ Swarm.Language.Pretty: class UnchainableFun t
+ Swarm.Language.Pretty: data Prec a
+ Swarm.Language.Pretty: encloseWithIndent :: Int -> Doc ann -> Doc ann -> Doc ann -> Doc ann
+ Swarm.Language.Pretty: instance (Swarm.Language.Pretty.UnchainableFun t, Swarm.Language.Pretty.PrettyPrec t) => Swarm.Language.Pretty.PrettyPrec (Swarm.Language.Types.TypeF t)
+ Swarm.Language.Pretty: instance Swarm.Language.Pretty.UnchainableFun (Control.Unification.Types.UTerm Swarm.Language.Types.TypeF ty)
+ Swarm.Language.Pretty: instance Swarm.Language.Pretty.UnchainableFun Swarm.Language.Types.Type
+ Swarm.Language.Pretty: prettyDefinition :: Doc ann -> Var -> Maybe Polytype -> Term -> Doc ann
+ Swarm.Language.Pretty: prettyLambda :: (Pretty a1, PrettyPrec a2) => (a1, Maybe a2) -> Doc ann
+ Swarm.Language.Pretty: prettyLambdas :: Term -> Doc a
+ Swarm.Language.Pretty: prettyTextLine :: PrettyPrec a => a -> Text
+ Swarm.Language.Pretty: unchainFun :: UnchainableFun t => t -> [t]
+ Swarm.Language.Pretty: unchainLambdas :: Term -> (Term, [(Var, Maybe Type)])
+ Swarm.Language.Syntax: Ignite :: Const
+ Swarm.Language.Syntax: Path :: Const
+ Swarm.Language.Syntax: Ping :: Const
+ Swarm.Language.Syntax: instance Data.Aeson.Types.FromJSON.FromJSONKey Swarm.Language.Syntax.Const
+ Swarm.Language.Syntax: instance Data.Aeson.Types.ToJSON.ToJSONKey Swarm.Language.Syntax.Const
+ Swarm.Language.Syntax: maxPathRange :: Integer
+ Swarm.Log: Critical :: Severity
+ Swarm.Log: Debug :: Severity
+ Swarm.Log: Error :: Severity
+ Swarm.Log: Info :: Severity
+ Swarm.Log: LogEntry :: TickNumber -> LogSource -> Severity -> Text -> Text -> LogEntry
+ Swarm.Log: Logged :: RobotLogSource
+ Swarm.Log: RobotError :: RobotLogSource
+ Swarm.Log: RobotLog :: RobotLogSource -> Int -> Cosmic Location -> LogSource
+ Swarm.Log: Said :: RobotLogSource
+ Swarm.Log: SystemLog :: LogSource
+ Swarm.Log: Warning :: Severity
+ Swarm.Log: [_leName] :: LogEntry -> Text
+ Swarm.Log: [_leSeverity] :: LogEntry -> Severity
+ Swarm.Log: [_leSource] :: LogEntry -> LogSource
+ Swarm.Log: [_leText] :: LogEntry -> Text
+ Swarm.Log: [_leTime] :: LogEntry -> TickNumber
+ Swarm.Log: data LogEntry
+ Swarm.Log: data LogSource
+ Swarm.Log: data RobotLogSource
+ Swarm.Log: data Severity
+ Swarm.Log: instance Data.Aeson.Types.FromJSON.FromJSON Swarm.Log.LogEntry
+ Swarm.Log: instance Data.Aeson.Types.FromJSON.FromJSON Swarm.Log.LogSource
+ Swarm.Log: instance Data.Aeson.Types.FromJSON.FromJSON Swarm.Log.RobotLogSource
+ Swarm.Log: instance Data.Aeson.Types.FromJSON.FromJSON Swarm.Log.Severity
+ Swarm.Log: instance Data.Aeson.Types.ToJSON.ToJSON Swarm.Log.LogEntry
+ Swarm.Log: instance Data.Aeson.Types.ToJSON.ToJSON Swarm.Log.LogSource
+ Swarm.Log: instance Data.Aeson.Types.ToJSON.ToJSON Swarm.Log.RobotLogSource
+ Swarm.Log: instance Data.Aeson.Types.ToJSON.ToJSON Swarm.Log.Severity
+ Swarm.Log: instance GHC.Classes.Eq Swarm.Log.LogEntry
+ Swarm.Log: instance GHC.Classes.Eq Swarm.Log.LogSource
+ Swarm.Log: instance GHC.Classes.Eq Swarm.Log.RobotLogSource
+ Swarm.Log: instance GHC.Classes.Eq Swarm.Log.Severity
+ Swarm.Log: instance GHC.Classes.Ord Swarm.Log.LogEntry
+ Swarm.Log: instance GHC.Classes.Ord Swarm.Log.LogSource
+ Swarm.Log: instance GHC.Classes.Ord Swarm.Log.RobotLogSource
+ Swarm.Log: instance GHC.Classes.Ord Swarm.Log.Severity
+ Swarm.Log: instance GHC.Generics.Generic Swarm.Log.LogEntry
+ Swarm.Log: instance GHC.Generics.Generic Swarm.Log.LogSource
+ Swarm.Log: instance GHC.Generics.Generic Swarm.Log.RobotLogSource
+ Swarm.Log: instance GHC.Generics.Generic Swarm.Log.Severity
+ Swarm.Log: instance GHC.Show.Show Swarm.Log.LogEntry
+ Swarm.Log: instance GHC.Show.Show Swarm.Log.LogSource
+ Swarm.Log: instance GHC.Show.Show Swarm.Log.RobotLogSource
+ Swarm.Log: instance GHC.Show.Show Swarm.Log.Severity
+ Swarm.Log: leName :: Lens' LogEntry Text
+ Swarm.Log: leSeverity :: Lens' LogEntry Severity
+ Swarm.Log: leSource :: Lens' LogEntry LogSource
+ Swarm.Log: leText :: Lens' LogEntry Text
+ Swarm.Log: leTime :: Lens' LogEntry TickNumber
+ Swarm.TUI.Editor.Model: WorldOverdraw :: Bool -> Map Coords (TerrainWith EntityFacade) -> WorldOverdraw
+ Swarm.TUI.Editor.Model: [_worldOverdraw] :: WorldEditor n -> WorldOverdraw
+ Swarm.TUI.Editor.Model: data WorldOverdraw
+ Swarm.TUI.Editor.Model: worldOverdraw :: forall n_a7GM9. Lens' (WorldEditor n_a7GM9) WorldOverdraw
+ Swarm.TUI.Editor.Util: getEditorContentAt :: WorldOverdraw -> MultiWorld Int Entity -> Cosmic Coords -> (TerrainType, Maybe EntityPaint)
+ Swarm.TUI.Editor.Util: getEditorTerrainAt :: WorldOverdraw -> MultiWorld Int Entity -> Cosmic Coords -> TerrainType
+ Swarm.TUI.Editor.Util: getMapRectangle :: (d -> e) -> (Coords -> (TerrainType, Maybe d)) -> BoundsRectangle -> [[PCell e]]
+ Swarm.TUI.Model: REPLError :: Text -> REPLHistItem
+ Swarm.TUI.Model: REPLHistoryCache :: Name
+ Swarm.TUI.Model: REPLViewport :: Name
+ Swarm.TUI.Model: nameParts :: Lens' RuntimeState NameGenerator
+ Swarm.TUI.Model: replScroll :: ViewportScroll Name
+ Swarm.TUI.Model.Name: REPLHistoryCache :: Name
+ Swarm.TUI.Model.Name: REPLViewport :: Name
+ Swarm.TUI.Model.Repl: REPLError :: Text -> REPLHistItem
+ Swarm.TUI.Model.Repl: getSessionREPLHistoryItems :: REPLHistory -> Seq REPLHistItem
+ Swarm.TUI.View: drawWorldPane :: UIState -> GameState -> Widget Name
+ Swarm.TUI.View: renderDutyCycle :: GameState -> Robot -> Widget Name
+ Swarm.TUI.View.Attribute.Attr: blueAttr :: AttrName
+ Swarm.TUI.View.Attribute.Attr: boldAttr :: AttrName
+ Swarm.TUI.View.Attribute.Attr: customEditFocusedAttr :: AttrName
+ Swarm.TUI.View.Attribute.Attr: cyanAttr :: AttrName
+ Swarm.TUI.View.Attribute.Attr: defAttr :: AttrName
+ Swarm.TUI.View.Attribute.Attr: dimAttr :: AttrName
+ Swarm.TUI.View.Attribute.Attr: dirtAttr :: AttrName
+ Swarm.TUI.View.Attribute.Attr: entityAttr :: AttrName
+ Swarm.TUI.View.Attribute.Attr: grassAttr :: AttrName
+ Swarm.TUI.View.Attribute.Attr: greenAttr :: AttrName
+ Swarm.TUI.View.Attribute.Attr: highlightAttr :: AttrName
+ Swarm.TUI.View.Attribute.Attr: iceAttr :: AttrName
+ Swarm.TUI.View.Attribute.Attr: infoAttr :: AttrName
+ Swarm.TUI.View.Attribute.Attr: italicAttr :: AttrName
+ Swarm.TUI.View.Attribute.Attr: lightCyanAttr :: AttrName
+ Swarm.TUI.View.Attribute.Attr: magentaAttr :: AttrName
+ Swarm.TUI.View.Attribute.Attr: meterAttributeNames :: NonEmpty AttrName
+ Swarm.TUI.View.Attribute.Attr: notifAttr :: AttrName
+ Swarm.TUI.View.Attribute.Attr: plantAttr :: AttrName
+ Swarm.TUI.View.Attribute.Attr: redAttr :: AttrName
+ Swarm.TUI.View.Attribute.Attr: robotAttr :: AttrName
+ Swarm.TUI.View.Attribute.Attr: rockAttr :: AttrName
+ Swarm.TUI.View.Attribute.Attr: stoneAttr :: AttrName
+ Swarm.TUI.View.Attribute.Attr: swarmAttrMap :: AttrMap
+ Swarm.TUI.View.Attribute.Attr: toAttrName :: Attribute -> AttrName
+ Swarm.TUI.View.Attribute.Attr: waterAttr :: AttrName
+ Swarm.TUI.View.Attribute.Attr: worldAttributeNames :: NonEmpty AttrName
+ Swarm.TUI.View.Attribute.Attr: worldPrefix :: AttrName
+ Swarm.TUI.View.Attribute.Attr: yellowAttr :: AttrName
+ Swarm.TUI.View.Attribute.CustomStyling: hexToAttrColor :: HexColor -> Color
+ Swarm.TUI.View.Attribute.CustomStyling: toAttrPair :: CustomAttr -> (AttrName, Attr)
+ Swarm.TUI.View.Attribute.CustomStyling: toStyle :: StyleFlag -> Style
+ Swarm.TUI.View.Attribute.Util: bgWithAutoForeground :: Kolor -> Attr
+ Swarm.TUI.View.Attribute.Util: kolorToAttrColor :: Kolor -> Color
+ Swarm.TUI.View.CellDisplay: EntityKnowledgeDependencies :: Bool -> [Text] -> Maybe Robot -> EntityKnowledgeDependencies
+ Swarm.TUI.View.CellDisplay: RenderingInput :: MultiWorld Int Entity -> (EntityPaint -> Bool) -> RenderingInput
+ Swarm.TUI.View.CellDisplay: [globallyKnownEntities] :: EntityKnowledgeDependencies -> [Text]
+ Swarm.TUI.View.CellDisplay: [isCreativeMode] :: EntityKnowledgeDependencies -> Bool
+ Swarm.TUI.View.CellDisplay: [isKnownFunc] :: RenderingInput -> EntityPaint -> Bool
+ Swarm.TUI.View.CellDisplay: [multiworldInfo] :: RenderingInput -> MultiWorld Int Entity
+ Swarm.TUI.View.CellDisplay: [theFocusedRobot] :: EntityKnowledgeDependencies -> Maybe Robot
+ Swarm.TUI.View.CellDisplay: data EntityKnowledgeDependencies
+ Swarm.TUI.View.CellDisplay: data RenderingInput
+ Swarm.TUI.View.CellDisplay: getEntityIsKnown :: EntityKnowledgeDependencies -> EntityPaint -> Bool
+ Swarm.TUI.View.CellDisplay: mkEntityKnowledge :: GameState -> EntityKnowledgeDependencies
+ Swarm.Util: hoistMaybe :: Applicative m => Maybe b -> MaybeT m b
+ Swarm.Util: listEnumsNonempty :: (Enum e, Bounded e) => NonEmpty e
+ Swarm.Util: showEnum :: (Show e, Enum e) => e -> NonEmpty Char
+ Swarm.Util: unsnocNE :: NonEmpty a -> ([a], a)
+ Swarm.Util.UnitInterval: data UnitInterval a
+ Swarm.Util.UnitInterval: getValue :: UnitInterval a -> a
+ Swarm.Util.UnitInterval: mkInterval :: (Ord a, Num a) => a -> UnitInterval a
+ Swarm.Util.UnitInterval: safeIndex :: RealFrac a => UnitInterval a -> NonEmpty b -> b
+ Swarm.Util.WindowedCounter: class Offsettable a
+ Swarm.Util.WindowedCounter: data WindowedCounter a
+ Swarm.Util.WindowedCounter: discardGarbage :: (Ord a, Offsettable a) => a -> WindowedCounter a -> WindowedCounter a
+ Swarm.Util.WindowedCounter: getOccupancy :: (Ord a, Offsettable a) => a -> WindowedCounter a -> UnitInterval Double
+ Swarm.Util.WindowedCounter: insert :: (Ord a, Offsettable a) => a -> WindowedCounter a -> WindowedCounter a
+ Swarm.Util.WindowedCounter: instance Data.Aeson.Types.FromJSON.FromJSON (Swarm.Util.WindowedCounter.WindowedCounter a)
+ Swarm.Util.WindowedCounter: instance Data.Aeson.Types.ToJSON.ToJSON a => Data.Aeson.Types.ToJSON.ToJSON (Swarm.Util.WindowedCounter.WindowedCounter a)
+ Swarm.Util.WindowedCounter: instance GHC.Classes.Eq a => GHC.Classes.Eq (Swarm.Util.WindowedCounter.WindowedCounter a)
+ Swarm.Util.WindowedCounter: instance GHC.Show.Show a => GHC.Show.Show (Swarm.Util.WindowedCounter.WindowedCounter a)
+ Swarm.Util.WindowedCounter: mkWindow :: Int -> WindowedCounter a
+ Swarm.Util.WindowedCounter: offsetBy :: Offsettable a => Int -> a -> a
+ Swarm.Web: instance Servant.Docs.Internal.ToSample Swarm.Web.RobotID
+ Swarm.Web: swarmApiHtml :: ByteString
+ Swarm.Web: swarmApiMarkdown :: String
- Swarm.Game.Achievement.Definitions: AchievementInfo :: Text -> Maybe FlavorText -> Text -> ExpectedEffort -> Bool -> AchievementInfo
+ Swarm.Game.Achievement.Definitions: AchievementInfo :: Text -> Maybe FlavorText -> Document Syntax -> ExpectedEffort -> Bool -> AchievementInfo
- Swarm.Game.Achievement.Definitions: Freeform :: Text -> FlavorText
+ Swarm.Game.Achievement.Definitions: Freeform :: Document Syntax -> FlavorText
- Swarm.Game.Achievement.Definitions: [attainmentProcess] :: AchievementInfo -> Text
+ Swarm.Game.Achievement.Definitions: [attainmentProcess] :: AchievementInfo -> Document Syntax
- Swarm.Game.CESK: Blackhole :: Term -> Env -> Cell
+ Swarm.Game.CESK: Blackhole :: Term -> Env -> MemCell
- Swarm.Game.CESK: E :: Term -> Env -> Cell
+ Swarm.Game.CESK: E :: Term -> Env -> MemCell
- Swarm.Game.CESK: TickNumber :: Integer -> TickNumber
+ Swarm.Game.CESK: TickNumber :: Int64 -> TickNumber
- Swarm.Game.CESK: V :: Value -> Cell
+ Swarm.Game.CESK: V :: Value -> MemCell
- Swarm.Game.CESK: [getTickNumber] :: TickNumber -> Integer
+ Swarm.Game.CESK: [getTickNumber] :: TickNumber -> Int64
- Swarm.Game.CESK: addTicks :: Integer -> TickNumber -> TickNumber
+ Swarm.Game.CESK: addTicks :: Int -> TickNumber -> TickNumber
- Swarm.Game.Entity: entityName :: Lens' Entity Text
+ Swarm.Game.Entity: entityName :: Lens' Entity EntityName
- Swarm.Game.Robot: mkRobot :: RobotID phase -> Maybe Int -> Text -> Document Syntax -> RobotLocation phase -> Heading -> Display -> CESK -> [Entity] -> [(Count, Entity)] -> Bool -> Bool -> TimeSpec -> RobotR phase
+ Swarm.Game.Robot: mkRobot :: RobotID phase -> Maybe Int -> Text -> Document Syntax -> RobotLocation phase -> Heading -> Display -> CESK -> [Entity] -> [(Count, Entity)] -> Bool -> Bool -> Set EntityName -> TimeSpec -> RobotR phase
- Swarm.Game.Scenario: Scenario :: Int -> Text -> Maybe Text -> Text -> Bool -> Maybe Int -> [CustomAttr] -> EntityMap -> [Recipe Entity] -> [Text] -> NonEmpty WorldDescription -> Navigation (Map SubworldName) Location -> [TRobot] -> [Objective] -> Maybe ProcessedTerm -> Maybe Int -> Scenario
+ Swarm.Game.Scenario: Scenario :: Int -> Text -> Maybe Text -> Document Syntax -> Bool -> Maybe Int -> [CustomAttr] -> EntityMap -> [Recipe Entity] -> [Text] -> NonEmpty WorldDescription -> Navigation (Map SubworldName) Location -> [TRobot] -> [Objective] -> Maybe ProcessedTerm -> Maybe Int -> Scenario
- Swarm.Game.Scenario: [_scenarioDescription] :: Scenario -> Text
+ Swarm.Game.Scenario: [_scenarioDescription] :: Scenario -> Document Syntax
- Swarm.Game.Scenario: scenarioDescription :: Lens' Scenario Text
+ Swarm.Game.Scenario: scenarioDescription :: Lens' Scenario (Document Syntax)
- Swarm.Game.State: GameStateConfig :: Array Int Text -> Array Int Text -> EntityMap -> [Recipe Entity] -> WorldMap -> GameStateConfig
+ Swarm.Game.State: GameStateConfig :: NameGenerator -> EntityMap -> [Recipe Entity] -> WorldMap -> GameStateConfig
- Swarm.Game.State: allDiscoveredEntities :: Lens' GameState Inventory
+ Swarm.Game.State: allDiscoveredEntities :: Lens' Discovery Inventory
- Swarm.Game.State: announcementQueue :: Lens' GameState (Seq Announcement)
+ Swarm.Game.State: announcementQueue :: Lens' Messages (Seq Announcement)
- Swarm.Game.State: availableCommands :: Lens' GameState (Notifications Const)
+ Swarm.Game.State: availableCommands :: Lens' Discovery (Notifications Const)
- Swarm.Game.State: availableRecipes :: Lens' GameState (Notifications (Recipe Entity))
+ Swarm.Game.State: availableRecipes :: Lens' Discovery (Notifications (Recipe Entity))
- Swarm.Game.State: entityMap :: Lens' GameState EntityMap
+ Swarm.Game.State: entityMap :: Lens' Landscape EntityMap
- Swarm.Game.State: gameAchievements :: Lens' GameState (Map GameplayAchievement Attainment)
+ Swarm.Game.State: gameAchievements :: Lens' Discovery (Map GameplayAchievement Attainment)
- Swarm.Game.State: gameStep :: Lens' GameState Step
+ Swarm.Game.State: gameStep :: Lens' TemporalState Step
- Swarm.Game.State: gensym :: Lens' GameState Int
+ Swarm.Game.State: gensym :: Lens' RobotNaming Int
- Swarm.Game.State: initiallyRunCode :: Lens' GameState (Maybe ProcessedTerm)
+ Swarm.Game.State: initiallyRunCode :: Lens' GameControls (Maybe ProcessedTerm)
- Swarm.Game.State: inputHandler :: Lens' GameState (Maybe (Text, Value))
+ Swarm.Game.State: inputHandler :: Lens' GameControls (Maybe (Text, Value))
- Swarm.Game.State: knownEntities :: Lens' GameState [Text]
+ Swarm.Game.State: knownEntities :: Lens' Discovery [Text]
- Swarm.Game.State: lastSeenMessageTime :: Lens' GameState TickNumber
+ Swarm.Game.State: lastSeenMessageTime :: Lens' Messages TickNumber
- Swarm.Game.State: messageQueue :: Lens' GameState (Seq LogEntry)
+ Swarm.Game.State: messageQueue :: Lens' Messages (Seq LogEntry)
- Swarm.Game.State: multiWorld :: Lens' GameState (MultiWorld Int Entity)
+ Swarm.Game.State: multiWorld :: Lens' Landscape (MultiWorld Int Entity)
- Swarm.Game.State: notificationsContent :: forall a_a7Nb2 a_a7NDC. Lens (Notifications a_a7Nb2) (Notifications a_a7NDC) [a_a7Nb2] [a_a7NDC]
+ Swarm.Game.State: notificationsContent :: forall a_a86Up a_a87mY. Lens (Notifications a_a86Up) (Notifications a_a87mY) [a_a86Up] [a_a87mY]
- Swarm.Game.State: notificationsCount :: forall a_a7Nb2. Lens' (Notifications a_a7Nb2) Int
+ Swarm.Game.State: notificationsCount :: forall a_a86Up. Lens' (Notifications a_a86Up) Int
- Swarm.Game.State: paused :: Getter GameState Bool
+ Swarm.Game.State: paused :: Getter TemporalState Bool
- Swarm.Game.State: recipesIn :: Lens' GameState (IntMap [Recipe Entity])
+ Swarm.Game.State: recipesIn :: Lens' Recipes (IntMap [Recipe Entity])
- Swarm.Game.State: recipesOut :: Lens' GameState (IntMap [Recipe Entity])
+ Swarm.Game.State: recipesOut :: Lens' Recipes (IntMap [Recipe Entity])
- Swarm.Game.State: replNextValueIndex :: Lens' GameState Integer
+ Swarm.Game.State: replNextValueIndex :: Lens' GameControls Integer
- Swarm.Game.State: replStatus :: Lens' GameState REPLStatus
+ Swarm.Game.State: replStatus :: Lens' GameControls REPLStatus
- Swarm.Game.State: replWorking :: Getter GameState Bool
+ Swarm.Game.State: replWorking :: Getter GameControls Bool
- Swarm.Game.State: robotStepsPerTick :: Lens' GameState Int
+ Swarm.Game.State: robotStepsPerTick :: Lens' TemporalState Int
- Swarm.Game.State: runStatus :: Lens' GameState RunStatus
+ Swarm.Game.State: runStatus :: Lens' TemporalState RunStatus
- Swarm.Game.State: ticks :: Lens' GameState TickNumber
+ Swarm.Game.State: ticks :: Lens' TemporalState TickNumber
- Swarm.Game.State: viewingRegion :: GameState -> (Int32, Int32) -> Cosmic BoundsRectangle
+ Swarm.Game.State: viewingRegion :: Cosmic Location -> (Int32, Int32) -> Cosmic BoundsRectangle
- Swarm.Game.State: worldNavigation :: Lens' GameState (Navigation (Map SubworldName) Location)
+ Swarm.Game.State: worldNavigation :: Lens' Landscape (Navigation (Map SubworldName) Location)
- Swarm.Game.State: worldScrollable :: Lens' GameState Bool
+ Swarm.Game.State: worldScrollable :: Lens' Landscape Bool
- Swarm.Game.Step: createLogEntry :: (Has (State GameState) sig m, Has (State Robot) sig m) => LogSource -> Text -> m LogEntry
+ Swarm.Game.Step: createLogEntry :: (Has (State GameState) sig m, Has (State Robot) sig m) => RobotLogSource -> Severity -> Text -> m LogEntry
- Swarm.Game.Step: onTarget :: HasRobotStepState sig m => RID -> (forall sig' m'. HasRobotStepState sig' m' => m' ()) -> m ()
+ Swarm.Game.Step: onTarget :: (HasRobotStepState sig m, Has (Lift IO) sig m) => RID -> (forall sig' m'. (HasRobotStepState sig' m', Has (Lift IO) sig' m') => m' ()) -> m ()
- Swarm.Game.Step: traceLog :: (Has (State GameState) sig m, Has (State Robot) sig m) => LogSource -> Text -> m LogEntry
+ Swarm.Game.Step: traceLog :: (Has (State GameState) sig m, Has (State Robot) sig m) => RobotLogSource -> Severity -> Text -> m LogEntry
- Swarm.Game.Universe: planar :: forall a_a3lgg a_a3lCq. Lens (Cosmic a_a3lgg) (Cosmic a_a3lCq) a_a3lgg a_a3lCq
+ Swarm.Game.Universe: planar :: forall a_a3tdX a_a3tA7. Lens (Cosmic a_a3tdX) (Cosmic a_a3tA7) a_a3tdX a_a3tA7
- Swarm.Game.Universe: subworld :: forall a_a3lgg. Lens' (Cosmic a_a3lgg) SubworldName
+ Swarm.Game.Universe: subworld :: forall a_a3tdX. Lens' (Cosmic a_a3tdX) SubworldName
- Swarm.Game.World: update :: Coords -> (Maybe Entity -> Maybe Entity) -> World t Entity -> (World t Entity, Bool)
+ Swarm.Game.World: update :: Coords -> (Maybe Entity -> Maybe Entity) -> World t Entity -> (World t Entity, CellUpdate Entity)
- Swarm.Game.World: updateM :: forall t sig m. (Has (State (World t Entity)) sig m, IArray UArray t) => Coords -> (Maybe Entity -> Maybe Entity) -> m Bool
+ Swarm.Game.World: updateM :: forall t sig m. (Has (State (World t Entity)) sig m, IArray UArray t) => Coords -> (Maybe Entity -> Maybe Entity) -> m (CellUpdate Entity)
- Swarm.Language.Syntax: sLoc :: forall ty_a1sLY. Lens' (Syntax' ty_a1sLY) SrcLoc
+ Swarm.Language.Syntax: sLoc :: forall ty_a1yS4. Lens' (Syntax' ty_a1yS4) SrcLoc
- Swarm.Language.Syntax: sTerm :: forall ty_a1sLY. Lens' (Syntax' ty_a1sLY) (Term' ty_a1sLY)
+ Swarm.Language.Syntax: sTerm :: forall ty_a1yS4. Lens' (Syntax' ty_a1yS4) (Term' ty_a1yS4)
- Swarm.Language.Syntax: sType :: forall ty_a1sLY. Lens' (Syntax' ty_a1sLY) ty_a1sLY
+ Swarm.Language.Syntax: sType :: forall ty_a1yS4. Lens' (Syntax' ty_a1yS4) ty_a1yS4
- Swarm.Language.Typed: polytype :: forall v_a3dP3. Lens' (Typed v_a3dP3) Polytype
+ Swarm.Language.Typed: polytype :: forall v_a3lLf. Lens' (Typed v_a3lLf) Polytype
- Swarm.Language.Typed: requires :: forall v_a3dP3. Lens' (Typed v_a3dP3) Requirements
+ Swarm.Language.Typed: requires :: forall v_a3lLf. Lens' (Typed v_a3lLf) Requirements
- Swarm.Language.Typed: value :: forall v_a3dP3 v_a3e3W. Lens (Typed v_a3dP3) (Typed v_a3e3W) v_a3dP3 v_a3e3W
+ Swarm.Language.Typed: value :: forall v_a3lLf v_a3m08. Lens (Typed v_a3lLf) (Typed v_a3m08) v_a3lLf v_a3m08
- Swarm.TUI.Border: bottomLabels :: forall n_aXPK. Lens' (BorderLabels n_aXPK) (HBorderLabels n_aXPK)
+ Swarm.TUI.Border: bottomLabels :: forall n_a13GU. Lens' (BorderLabels n_a13GU) (HBorderLabels n_a13GU)
- Swarm.TUI.Border: centerLabel :: forall n_aXPL. Lens' (HBorderLabels n_aXPL) (Maybe (Widget n_aXPL))
+ Swarm.TUI.Border: centerLabel :: forall n_a13GV. Lens' (HBorderLabels n_a13GV) (Maybe (Widget n_a13GV))
- Swarm.TUI.Border: leftLabel :: forall n_aXPL. Lens' (HBorderLabels n_aXPL) (Maybe (Widget n_aXPL))
+ Swarm.TUI.Border: leftLabel :: forall n_a13GV. Lens' (HBorderLabels n_a13GV) (Maybe (Widget n_a13GV))
- Swarm.TUI.Border: rightLabel :: forall n_aXPL. Lens' (HBorderLabels n_aXPL) (Maybe (Widget n_aXPL))
+ Swarm.TUI.Border: rightLabel :: forall n_a13GV. Lens' (HBorderLabels n_a13GV) (Maybe (Widget n_a13GV))
- Swarm.TUI.Border: topLabels :: forall n_aXPK. Lens' (BorderLabels n_aXPK) (HBorderLabels n_aXPK)
+ Swarm.TUI.Border: topLabels :: forall n_a13GU. Lens' (BorderLabels n_a13GU) (HBorderLabels n_a13GU)
- Swarm.TUI.Editor.Json: SkeletonScenario :: Int -> Text -> Text -> Bool -> [Entity] -> WorldDescriptionPaint -> [String] -> SkeletonScenario
+ Swarm.TUI.Editor.Json: SkeletonScenario :: Int -> Text -> Document Syntax -> Bool -> [Entity] -> WorldDescriptionPaint -> [String] -> SkeletonScenario
- Swarm.TUI.Editor.Json: [description] :: SkeletonScenario -> Text
+ Swarm.TUI.Editor.Json: [description] :: SkeletonScenario -> Document Syntax
- Swarm.TUI.Editor.Model: WorldEditor :: Bool -> List n TerrainType -> List n EntityFacade -> Map Coords (TerrainWith EntityFacade) -> MapEditingBounds -> FocusRing n -> FilePath -> Maybe String -> WorldEditor n
+ Swarm.TUI.Editor.Model: WorldEditor :: WorldOverdraw -> List n TerrainType -> List n EntityFacade -> MapEditingBounds -> FocusRing n -> FilePath -> Maybe String -> WorldEditor n
- Swarm.TUI.Editor.Model: [_isWorldEditorEnabled] :: WorldEditor n -> Bool
+ Swarm.TUI.Editor.Model: [_isWorldEditorEnabled] :: WorldOverdraw -> Bool
- Swarm.TUI.Editor.Model: [_paintedTerrain] :: WorldEditor n -> Map Coords (TerrainWith EntityFacade)
+ Swarm.TUI.Editor.Model: [_paintedTerrain] :: WorldOverdraw -> Map Coords (TerrainWith EntityFacade)
- Swarm.TUI.Editor.Model: editingBounds :: forall n_a7o6s. Lens' (WorldEditor n_a7o6s) MapEditingBounds
+ Swarm.TUI.Editor.Model: editingBounds :: forall n_a7GM9. Lens' (WorldEditor n_a7GM9) MapEditingBounds
- Swarm.TUI.Editor.Model: editorFocusRing :: forall n_a7o6s. Lens' (WorldEditor n_a7o6s) (FocusRing n_a7o6s)
+ Swarm.TUI.Editor.Model: editorFocusRing :: forall n_a7GM9. Lens' (WorldEditor n_a7GM9) (FocusRing n_a7GM9)
- Swarm.TUI.Editor.Model: entityPaintList :: forall n_a7o6s. Lens' (WorldEditor n_a7o6s) (List n_a7o6s EntityFacade)
+ Swarm.TUI.Editor.Model: entityPaintList :: forall n_a7GM9. Lens' (WorldEditor n_a7GM9) (List n_a7GM9 EntityFacade)
- Swarm.TUI.Editor.Model: isWorldEditorEnabled :: forall n_a7o6s. Lens' (WorldEditor n_a7o6s) Bool
+ Swarm.TUI.Editor.Model: isWorldEditorEnabled :: Lens' WorldOverdraw Bool
- Swarm.TUI.Editor.Model: lastWorldEditorMessage :: forall n_a7o6s. Lens' (WorldEditor n_a7o6s) (Maybe String)
+ Swarm.TUI.Editor.Model: lastWorldEditorMessage :: forall n_a7GM9. Lens' (WorldEditor n_a7GM9) (Maybe String)
- Swarm.TUI.Editor.Model: outputFilePath :: forall n_a7o6s. Lens' (WorldEditor n_a7o6s) FilePath
+ Swarm.TUI.Editor.Model: outputFilePath :: forall n_a7GM9. Lens' (WorldEditor n_a7GM9) FilePath
- Swarm.TUI.Editor.Model: paintedTerrain :: forall n_a7o6s. Lens' (WorldEditor n_a7o6s) (Map Coords (TerrainWith EntityFacade))
+ Swarm.TUI.Editor.Model: paintedTerrain :: Lens' WorldOverdraw (Map Coords (TerrainWith EntityFacade))
- Swarm.TUI.Editor.Model: terrainList :: forall n_a7o6s. Lens' (WorldEditor n_a7o6s) (List n_a7o6s TerrainType)
+ Swarm.TUI.Editor.Model: terrainList :: forall n_a7GM9. Lens' (WorldEditor n_a7GM9) (List n_a7GM9 TerrainType)
- Swarm.TUI.Editor.Util: getContentAt :: WorldEditor Name -> MultiWorld Int Entity -> Cosmic Coords -> (TerrainType, Maybe EntityPaint)
+ Swarm.TUI.Editor.Util: getContentAt :: MultiWorld Int e -> Cosmic Coords -> (TerrainType, Maybe e)
- Swarm.TUI.Editor.Util: getEditedMapRectangle :: WorldEditor Name -> Maybe (Cosmic BoundsRectangle) -> MultiWorld Int Entity -> [[CellPaintDisplay]]
+ Swarm.TUI.Editor.Util: getEditedMapRectangle :: WorldOverdraw -> Maybe (Cosmic BoundsRectangle) -> MultiWorld Int Entity -> [[CellPaintDisplay]]
- Swarm.TUI.Model: logEvent :: LogSource -> (Text, RID) -> Text -> Notifications LogEntry -> Notifications LogEntry
+ Swarm.TUI.Model: logEvent :: LogSource -> Severity -> Text -> Text -> Notifications LogEntry -> Notifications LogEntry
- Swarm.TUI.Model.UI: UIState :: Menu -> Bool -> Bool -> FocusRing Name -> LaunchOptions -> Maybe (Cosmic Coords) -> WorldEditor Name -> REPLState -> Maybe (Int, List Name InventoryListEntry) -> InventorySortOptions -> Maybe Text -> Bool -> Bool -> Bool -> Maybe Text -> Maybe Modal -> GoalDisplay -> Bool -> Map CategorizedAchievement Attainment -> Bool -> Bool -> Bool -> Bool -> TimeSpec -> Bool -> Double -> Double -> Int -> Int -> Int -> Int -> TimeSpec -> TimeSpec -> TimeSpec -> AttrMap -> Maybe ScenarioInfoPair -> UIState
+ Swarm.TUI.Model.UI: UIState :: Menu -> Bool -> Bool -> FocusRing Name -> LaunchOptions -> Maybe (Cosmic Coords) -> WorldEditor Name -> REPLState -> Maybe (Int, List Name InventoryListEntry) -> InventorySortOptions -> Maybe Text -> Bool -> Maybe Modal -> GoalDisplay -> Bool -> Map CategorizedAchievement Attainment -> Bool -> Bool -> Bool -> Bool -> TimeSpec -> Bool -> Double -> Double -> Int -> Int -> Int -> Int -> TimeSpec -> TimeSpec -> TimeSpec -> AttrMap -> Maybe ScenarioInfoPair -> UIState
- Swarm.TUI.View.CellDisplay: displayEntityCell :: WorldEditor Name -> GameState -> Cosmic Coords -> [Display]
+ Swarm.TUI.View.CellDisplay: displayEntityCell :: WorldOverdraw -> RenderingInput -> Cosmic Coords -> [Display]
- Swarm.TUI.View.CellDisplay: displayLoc :: Bool -> WorldEditor Name -> GameState -> Cosmic Coords -> Display
+ Swarm.TUI.View.CellDisplay: displayLoc :: Bool -> WorldOverdraw -> GameState -> Cosmic Coords -> Display
- Swarm.TUI.View.CellDisplay: displayLocRaw :: Bool -> WorldEditor Name -> GameState -> Cosmic Coords -> Display
+ Swarm.TUI.View.CellDisplay: displayLocRaw :: WorldOverdraw -> RenderingInput -> [Display] -> Cosmic Coords -> Display
- Swarm.TUI.View.CellDisplay: displayTerrainCell :: WorldEditor Name -> GameState -> Cosmic Coords -> Display
+ Swarm.TUI.View.CellDisplay: displayTerrainCell :: WorldOverdraw -> RenderingInput -> Cosmic Coords -> Display
Files
- CHANGELOG.md +37/−0
- app/Main.hs +36/−14
- bench/Benchmark.hs +0/−125
- data/entities.yaml +43/−17
- data/scenarios/Challenges/00-ORDER.txt +2/−0
- data/scenarios/Challenges/2048.yaml +3/−3
- data/scenarios/Challenges/Mazes/easy_cave_maze.yaml +1/−1
- data/scenarios/Challenges/Ranching/_gated-paddock/enclosure-checking.sw +3/−145
- data/scenarios/Challenges/Ranching/_gated-paddock/meandering-sheep.sw +1/−20
- data/scenarios/Challenges/Ranching/capture.yaml +1/−1
- data/scenarios/Challenges/Ranching/gated-paddock.yaml +15/−154
- data/scenarios/Challenges/Ranching/powerset.yaml +1/−1
- data/scenarios/Challenges/Sliding Puzzles/3x3.yaml +1/−1
- data/scenarios/Challenges/Sokoban/Gadgets/no-reverse.yaml +1/−1
- data/scenarios/Challenges/Sokoban/Gadgets/one-way.yaml +1/−1
- data/scenarios/Challenges/Sokoban/Simple/trapdoor.yaml +1/−1
- data/scenarios/Challenges/Sokoban/foresight.yaml +2/−2
- data/scenarios/Challenges/_combo-lock/setup.sw +85/−0
- data/scenarios/Challenges/_combo-lock/solution.sw +65/−0
- data/scenarios/Challenges/_wave/solution.sw +15/−0
- data/scenarios/Challenges/arbitrage.yaml +1/−1
- data/scenarios/Challenges/blender.yaml +3/−3
- data/scenarios/Challenges/bridge-building.yaml +14/−14
- data/scenarios/Challenges/bucket-brigade.yaml +5/−5
- data/scenarios/Challenges/chess_horse.yaml +2/−2
- data/scenarios/Challenges/combo-lock.yaml +135/−0
- data/scenarios/Challenges/friend.yaml +2/−0
- data/scenarios/Challenges/gopher.yaml +9/−7
- data/scenarios/Challenges/hackman.yaml +2/−2
- data/scenarios/Challenges/ice-cream.yaml +3/−3
- data/scenarios/Challenges/maypole.yaml +1/−1
- data/scenarios/Challenges/teleport.yaml +1/−1
- data/scenarios/Challenges/wave.yaml +89/−0
- data/scenarios/Challenges/wolf-goat-cabbage.yaml +3/−3
- data/scenarios/Challenges/word-search.yaml +2/−2
- data/scenarios/Speedruns/curry.yaml +1/−1
- data/scenarios/Speedruns/forester.yaml +2/−2
- data/scenarios/Speedruns/mithril.yaml +2/−2
- data/scenarios/Testing/00-ORDER.txt +7/−0
- data/scenarios/Testing/1341-command-count.yaml +107/−0
- data/scenarios/Testing/1355-combustion.yaml +133/−0
- data/scenarios/Testing/1430-built-robot-ownership.yaml +64/−0
- data/scenarios/Testing/1535-ping/00-ORDER.txt +2/−0
- data/scenarios/Testing/1535-ping/1535-in-range.yaml +87/−0
- data/scenarios/Testing/1535-ping/1535-out-of-range.yaml +67/−0
- data/scenarios/Testing/1535-ping/_1535-in-range/buddy.sw +41/−0
- data/scenarios/Testing/1535-ping/_1535-in-range/solution.sw +57/−0
- data/scenarios/Testing/1535-ping/_1535-out-of-range/solution.sw +11/−0
- data/scenarios/Testing/1536-custom-unwalkable-entities.yaml +50/−0
- data/scenarios/Testing/836-pathfinding/00-ORDER.txt +8/−0
- data/scenarios/Testing/836-pathfinding/836-automatic-waypoint-navigation.yaml +154/−0
- data/scenarios/Testing/836-pathfinding/836-no-path-exists1.yaml +48/−0
- data/scenarios/Testing/836-pathfinding/836-no-path-exists2.yaml +52/−0
- data/scenarios/Testing/836-pathfinding/836-path-exists-distance-limit-reachable.yaml +33/−0
- data/scenarios/Testing/836-pathfinding/836-path-exists-distance-limit-unreachable.yaml +35/−0
- data/scenarios/Testing/836-pathfinding/836-path-exists-find-entity-unwalkable.yaml +51/−0
- data/scenarios/Testing/836-pathfinding/836-path-exists-find-entity.yaml +47/−0
- data/scenarios/Testing/836-pathfinding/836-path-exists-find-location.yaml +47/−0
- data/scenarios/Testing/836-pathfinding/_836-automatic-waypoint-navigation/gardener.sw +18/−0
- data/scenarios/Testing/836-pathfinding/_836-automatic-waypoint-navigation/patrol.sw +35/−0
- data/scenarios/Testing/836-pathfinding/_836-automatic-waypoint-navigation/rabbit.sw +16/−0
- data/scenarios/Testing/836-pathfinding/_836-path-exists/find-entity-solution.sw +14/−0
- data/scenarios/Testing/836-pathfinding/_836-path-exists/find-entity-unwalkable-solution.sw +22/−0
- data/scenarios/Testing/836-pathfinding/_836-path-exists/find-location-solution.sw +14/−0
- data/scenarios/Testing/Achievements/00-ORDER.txt +1/−0
- data/scenarios/Testing/Achievements/RobotIntoWater.yaml +48/−0
- data/scenarios/Testing/_Validation/1221-duplicate-entities.yaml +1/−0
- data/scenarios/Tutorials/type-errors.yaml +1/−1
- data/scenarios/Tutorials/types.yaml +2/−1
- data/scenarios/Tutorials/world101.sw +36/−0
- data/scenarios/Tutorials/world101.yaml +5/−3
- editors/emacs/swarm-mode.el +3/−0
- editors/vim/swarm.lua +8/−0
- editors/vim/swarm.vim +24/−0
- editors/vscode/syntaxes/swarm.tmLanguage.json +1/−1
- src/Swarm/App.hs +6/−5
- src/Swarm/Constant.hs +5/−1
- src/Swarm/Doc/Gen.hs +60/−50
- src/Swarm/Doc/Pedagogy.hs +8/−3
- src/Swarm/Doc/Schema/Arrangement.hs +42/−0
- src/Swarm/Doc/Schema/Parse.hs +52/−0
- src/Swarm/Doc/Schema/Refined.hs +145/−0
- src/Swarm/Doc/Schema/Render.hs +194/−0
- src/Swarm/Doc/Schema/SchemaType.hs +38/−0
- src/Swarm/Doc/Util.hs +22/−0
- src/Swarm/Game/Achievement/Attainment.hs +14/−4
- src/Swarm/Game/Achievement/Definitions.hs +33/−7
- src/Swarm/Game/Achievement/Description.hs +86/−75
- src/Swarm/Game/Achievement/Persistence.hs +7/−5
- src/Swarm/Game/CESK.hs +37/−26
- src/Swarm/Game/Entity.hs +68/−9
- src/Swarm/Game/Exception.hs +28/−22
- src/Swarm/Game/Failure.hs +10/−2
- src/Swarm/Game/Location.hs +46/−23
- src/Swarm/Game/Log.hs +0/−76
- src/Swarm/Game/Recipe.hs +41/−24
- src/Swarm/Game/ResourceLoading.hs +28/−4
- src/Swarm/Game/Robot.hs +174/−64
- src/Swarm/Game/Scenario.hs +19/−3
- src/Swarm/Game/Scenario/Objective.hs +4/−4
- src/Swarm/Game/Scenario/Objective/Validation.hs +4/−3
- src/Swarm/Game/Scenario/Scoring/Best.hs +4/−1
- src/Swarm/Game/Scenario/Scoring/CodeSize.hs +5/−2
- src/Swarm/Game/Scenario/Scoring/ConcreteMetrics.hs +4/−1
- src/Swarm/Game/Scenario/Scoring/GenericMetrics.hs +7/−4
- src/Swarm/Game/Scenario/Status.hs +17/−10
- src/Swarm/Game/Scenario/Style.hs +4/−0
- src/Swarm/Game/Scenario/Topography/Area.hs +9/−4
- src/Swarm/Game/Scenario/Topography/Cell.hs +11/−9
- src/Swarm/Game/Scenario/Topography/EntityFacade.hs +5/−5
- src/Swarm/Game/Scenario/Topography/Navigation/Portal.hs +19/−4
- src/Swarm/Game/Scenario/Topography/Navigation/Util.hs +44/−0
- src/Swarm/Game/Scenario/Topography/Navigation/Waypoint.hs +16/−0
- src/Swarm/Game/Scenario/Topography/Placement.hs +3/−0
- src/Swarm/Game/Scenario/Topography/Structure.hs +6/−3
- src/Swarm/Game/ScenarioInfo.hs +6/−4
- src/Swarm/Game/State.hs +457/−297
- src/Swarm/Game/Step.hs +244/−313
- src/Swarm/Game/Step/Combustion.hs +227/−0
- src/Swarm/Game/Step/Pathfinding.hs +137/−0
- src/Swarm/Game/Step/Util.hs +224/−0
- src/Swarm/Game/Terrain.hs +3/−2
- src/Swarm/Game/Universe.hs +19/−3
- src/Swarm/Game/Value.hs +6/−2
- src/Swarm/Game/World.hs +10/−9
- src/Swarm/Game/World/Coords.hs +4/−4
- src/Swarm/Game/World/Modify.hs +40/−0
- src/Swarm/Game/World/Parse.hs +4/−11
- src/Swarm/Game/World/Render.hs +65/−0
- src/Swarm/Game/World/Typecheck.hs +2/−2
- src/Swarm/Language/Capability.hs +12/−1
- src/Swarm/Language/Direction.hs +4/−4
- src/Swarm/Language/Key.hs +4/−4
- src/Swarm/Language/LSP/Hover.hs +2/−0
- src/Swarm/Language/Parse.hs +4/−4
- src/Swarm/Language/Pretty.hs +117/−54
- src/Swarm/Language/Requirement.hs +6/−6
- src/Swarm/Language/Syntax.hs +50/−42
- src/Swarm/Language/Text/Markdown.hs +8/−7
- src/Swarm/Language/Typecheck.hs +28/−3
- src/Swarm/Log.hs +70/−0
- src/Swarm/ReadableIORef.hs +1/−1
- src/Swarm/TUI/Attr.hs +0/−189
- src/Swarm/TUI/Controller.hs +138/−135
- src/Swarm/TUI/Controller/Util.hs +5/−5
- src/Swarm/TUI/Editor/Controller.hs +14/−12
- src/Swarm/TUI/Editor/Json.hs +5/−1
- src/Swarm/TUI/Editor/Masking.hs +2/−0
- src/Swarm/TUI/Editor/Model.hs +13/−6
- src/Swarm/TUI/Editor/Palette.hs +3/−2
- src/Swarm/TUI/Editor/Util.hs +33/−21
- src/Swarm/TUI/Editor/View.hs +4/−2
- src/Swarm/TUI/Launch/Controller.hs +2/−2
- src/Swarm/TUI/Launch/Prep.hs +7/−6
- src/Swarm/TUI/Launch/View.hs +1/−1
- src/Swarm/TUI/Model.hs +17/−32
- src/Swarm/TUI/Model/Goal.hs +7/−3
- src/Swarm/TUI/Model/Menu.hs +4/−1
- src/Swarm/TUI/Model/Name.hs +10/−0
- src/Swarm/TUI/Model/Repl.hs +35/−18
- src/Swarm/TUI/Model/StateUpdate.hs +6/−6
- src/Swarm/TUI/Model/UI.hs +17/−33
- src/Swarm/TUI/View.hs +253/−121
- src/Swarm/TUI/View/Achievement.hs +6/−6
- src/Swarm/TUI/View/Attribute/Attr.hs +229/−0
- src/Swarm/TUI/View/Attribute/CustomStyling.hs +37/−0
- src/Swarm/TUI/View/Attribute/Util.hs +28/−0
- src/Swarm/TUI/View/CellDisplay.hs +81/−42
- src/Swarm/TUI/View/CustomStyling.hs +0/−38
- src/Swarm/TUI/View/Objective.hs +8/−4
- src/Swarm/TUI/View/Util.hs +1/−1
- src/Swarm/Util.hs +53/−2
- src/Swarm/Util/UnitInterval.hs +38/−0
- src/Swarm/Util/WindowedCounter.hs +189/−0
- src/Swarm/Version.hs +7/−1
- src/Swarm/Web.hs +140/−80
- swarm.cabal +50/−13
- test/bench/Benchmark.hs +168/−0
- test/integration/Main.hs +103/−17
- test/unit/TestLanguagePipeline.hs +37/−18
- test/unit/TestNotification.hs +8/−7
- test/unit/TestPretty.hs +12/−0
- test/unit/TestUtil.hs +1/−1
CHANGELOG.md view
@@ -1,5 +1,42 @@ # Revision history for swarm +## **0.5.0.0** - 2023-11-01++### Bugfixes++* Fix bug where some pretty-printed terms contained extra elaborated+ terms inserted by @byorgey ([#1497](https://github.com/swarm-game/swarm/pull/1497))++### New Features++#### Language++* New `path` command for pathfinding by @kostmo ([#1523](https://github.com/swarm-game/swarm/pull/1523))++#### New scenarios++* "Robot wave" scenario by @kostmo ([#1556](https://github.com/swarm-game/swarm/pull/1556))+* Combination locks scenario by @kostmo ([#1591](https://github.com/swarm-game/swarm/pull/1591))++#### New achievements++* Grant `RobotIntoWater` achievement by @byorgey ([#1504](https://github.com/swarm-game/swarm/pull/1504))+* Achievement for pointless swapping by @kostmo ([#1588](https://github.com/swarm-game/swarm/pull/1588))++#### UI enhancements++* Support Markdown in achievement descriptions by @kostmo ([#1508](https://github.com/swarm-game/swarm/pull/1508))+* Render map preview on scenario selection screen by @kostmo ([#1515](https://github.com/swarm-game/swarm/pull/1515))+* Robot activity counts in F2 menu by @kostmo ([#1484](https://github.com/swarm-game/swarm/pull/1484))+* Show robot IDs in F2 menu by @kostmo ([#1482](https://github.com/swarm-game/swarm/pull/1482))+* Print REPL errors inline and get rid of error popup by @byorgey ([#1487](https://github.com/swarm-game/swarm/pull/1487))+* Improvements to scrolling by @byorgey ([#1481](https://github.com/swarm-game/swarm/pull/1481))++#### Command line options++* Improvements to term pretty-printing by @xsebek ([#1464](https://github.com/swarm-game/swarm/pull/1464))+* `swarm format` now actually formats by @xsebek ([#1459](https://github.com/swarm-game/swarm/pull/1459))+ ## **0.4.0.0** - 2023-08-18 ### Bugfixes
app/Main.hs view
@@ -13,15 +13,21 @@ import Data.Text.IO qualified as Text import GitHash (GitInfo, giBranch, giHash, tGitInfoCwdTry) import Options.Applicative+import Prettyprinter+import Prettyprinter.Render.Text qualified as RT import Swarm.App (appMain) import Swarm.Doc.Gen (EditorType (..), GenerateDocs (..), PageAddress (..), SheetType (..), generateDocs)+import Swarm.Game.World.Render (printScenarioMap, renderScenarioMap) import Swarm.Language.LSP (lspMain)-import Swarm.Language.Pipeline (processTerm)+import Swarm.Language.Parse (readTerm)+import Swarm.Language.Pretty (ppr) import Swarm.TUI.Model (AppOpts (..), ColorMode (..)) import Swarm.TUI.Model.UI (defaultInitLgTicksPerSecond)+import Swarm.Util ((?)) import Swarm.Version import Swarm.Web (defaultPort)-import System.Exit (exitFailure, exitSuccess)+import System.Console.Terminal.Size qualified as Term+import System.Exit (exitFailure) import System.IO (hPrint, stderr) import Text.Read (readMaybe) @@ -33,10 +39,13 @@ Nothing -> "" Just git -> " (" <> giBranch git <> "@" <> take 10 (giHash git) <> ")" +type Width = Int+ data CLI = Run AppOpts- | Format Input+ | Format Input (Maybe Width) | DocGen GenerateDocs+ | RenderMap FilePath | LSP | Version @@ -44,8 +53,9 @@ cliParser = subparser ( mconcat- [ command "format" (info (format <**> helper) (progDesc "Format a file"))+ [ command "format" (info (Format <$> format <*> optional widthOpt <**> helper) (progDesc "Format a file")) , command "generate" (info (DocGen <$> docgen <**> helper) (progDesc "Generate docs"))+ , command "map" (info (RenderMap <$> strArgument (metavar "FILE")) (progDesc "Render a scenario world map.")) , command "lsp" (info (pure LSP) (progDesc "Start the LSP")) , command "version" (info (pure Version) (progDesc "Get current and upstream version.")) ]@@ -63,10 +73,12 @@ <*> pure gitInfo ) where- format :: Parser CLI+ format :: Parser Input format =- (Format Stdin <$ switch (long "stdin" <> help "Read code from stdin"))- <|> (Format . File <$> strArgument (metavar "FILE"))+ (Stdin <$ switch (long "stdin" <> help "Read code from stdin"))+ <|> (File <$> strArgument (metavar "FILE"))+ widthOpt :: Parser Width+ widthOpt = option auto (long "width" <> metavar "COLUMNS" <> help "Use layout with maximum width") docgen :: Parser GenerateDocs docgen = subparser . mconcat $@@ -75,7 +87,9 @@ , command "keys" (info (pure SpecialKeyNames) $ progDesc "Output list of recognized special key names") , command "cheatsheet" (info (CheatSheet <$> address <*> cheatsheet <**> helper) $ progDesc "Output nice Wiki tables") , command "pedagogy" (info (pure TutorialCoverage) $ progDesc "Output tutorial coverage")+ , command "endpoints" (info (pure WebAPIEndpoints) $ progDesc "Generate markdown Web API documentation.") ]+ editor :: Parser (Maybe EditorType) editor = Data.Foldable.asum@@ -103,6 +117,7 @@ , Just Recipes <$ switch (long "recipes" <> help "Generate recipes page (uses data from recipes.yaml)") , Just Capabilities <$ switch (long "capabilities" <> help "Generate capabilities page (uses entity map)") , Just Commands <$ switch (long "commands" <> help "Generate commands page (uses constInfo, constCaps and inferConst)")+ , Just Scenario <$ switch (long "scenario" <> help "Generate scenario schema page") ] seed :: Parser (Maybe Int) seed = optional $ option auto (long "seed" <> short 's' <> metavar "INT" <> help "Seed to use for world generation")@@ -157,13 +172,19 @@ showInput (File fp) = pack fp -- | Utility function to validate and format swarm-lang code-formatFile :: Input -> IO ()-formatFile input = do+formatFile :: Input -> Maybe Width -> IO ()+formatFile input mWidth = do content <- getInput input- case processTerm content of- Right _ -> do- Text.putStrLn content- exitSuccess+ case readTerm content of+ Right Nothing -> Text.putStrLn ""+ Right (Just ast) -> do+ mWindow <- Term.size+ let mkOpt w = LayoutOptions (AvailablePerLine w 1.0)+ let opt =+ fmap mkOpt mWidth+ ? fmap (\(Term.Window _h w) -> mkOpt w) mWindow+ ? defaultLayoutOptions+ Text.putStrLn . RT.renderStrict . layoutPretty opt $ ppr ast Left e -> do Text.hPutStrLn stderr $ showInput input <> ":" <> e exitFailure@@ -180,6 +201,7 @@ case cli of Run opts -> appMain opts DocGen g -> generateDocs g- Format fo -> formatFile fo+ Format fo w -> formatFile fo w+ RenderMap mapPath -> printScenarioMap =<< renderScenarioMap mapPath LSP -> lspMain Version -> showVersion
− bench/Benchmark.hs
@@ -1,125 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE QuasiQuotes #-}---- |--- SPDX-License-Identifier: BSD-3-Clause-module Main where--import Control.Lens ((&), (.~), (^.))-import Control.Monad (replicateM_)-import Control.Monad.Except (runExceptT)-import Control.Monad.State (evalStateT, execStateT)-import Criterion.Main (Benchmark, bench, bgroup, defaultConfig, defaultMainWith, whnfAppIO)-import Criterion.Types (Config (timeLimit))-import Data.Map qualified as M-import Swarm.Game.CESK (emptyStore, initMachine)-import Swarm.Game.Display (defaultRobotDisplay)-import Swarm.Game.Location-import Swarm.Game.Robot (TRobot, mkRobot)-import Swarm.Game.State (GameState, addTRobot, creativeMode, multiWorld)-import Swarm.Game.Step (gameTick)-import Swarm.Game.Terrain (TerrainType (DirtT))-import Swarm.Game.Universe (Cosmic (..), SubworldName (DefaultRootSubworld))-import Swarm.Game.World (WorldFun (..), newWorld)-import Swarm.Language.Context qualified as Context-import Swarm.Language.Pipeline (ProcessedTerm)-import Swarm.Language.Pipeline.QQ (tmQ)-import Swarm.TUI.Model (gameState)-import Swarm.TUI.Model.StateUpdate (classicGame0)-import Swarm.Util.Erasable---- | 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--- appearance, then waits another random number of ticks, places a tree, and--- then self-destructs.-treeProgram :: ProcessedTerm-treeProgram =- [tmQ|- {- r <- random 100;- wait (r + 300);- appear "|";- r <- random 100;- wait (r + 300);- place "tree";- selfdestruct- }- |]---- | The program of a robot that moves forward forever.-moverProgram :: ProcessedTerm-moverProgram =- [tmQ|- let forever : cmd unit -> cmd unit = \c. c; forever c- in forever move- |]---- | The program of a robot that moves in circles forever.-circlerProgram :: ProcessedTerm-circlerProgram =- [tmQ|- let forever : cmd unit -> cmd unit = \c. c; forever c- in forever (- move;- turn right;- move;- turn right;- move;- turn right;- move;- turn right;- )- |]---- | Initializes a robot with program prog at location loc facing north.-initRobot :: ProcessedTerm -> Location -> TRobot-initRobot prog loc = mkRobot () Nothing "" mempty (Just $ Cosmic DefaultRootSubworld 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 :: (Location -> TRobot) -> Int -> IO GameState-mkGameState robotMaker numRobots = do- let robots = [robotMaker (Location (fromIntegral x) 0) | x <- [0 .. numRobots - 1]]- Right initAppState <- runExceptT classicGame0- execStateT- (mapM addTRobot robots)- ( (initAppState ^. gameState)- & creativeMode .~ True- & multiWorld .~ M.singleton DefaultRootSubworld (newWorld (WF $ const (fromEnum DirtT, ENothing)))- )---- | Runs numGameTicks ticks of the game.-runGame :: Int -> GameState -> IO ()-runGame numGameTicks = evalStateT (replicateM_ numGameTicks gameTick)--main :: IO ()-main = do- idlers <- mkGameStates idleProgram [10, 20 .. 40]- trees <- mkGameStates treeProgram [10, 20 .. 40]- circlers <- mkGameStates circlerProgram [10, 20 .. 40]- movers <- mkGameStates moverProgram [10, 20 .. 40]- -- In theory we should force the evaluation of these game states to normal- -- form before running the benchmarks. In practice, the first of the many- -- criterion runs for each of these benchmarks doesn't look like an outlier.- defaultMainWith- (defaultConfig {timeLimit = 10})- [ bgroup- "run 1000 game ticks"- [ bgroup "idlers" (toBenchmarks idlers)- , bgroup "trees" (toBenchmarks trees)- , bgroup "circlers" (toBenchmarks circlers)- , bgroup "movers" (toBenchmarks movers)- ]- ]- where- mkGameStates :: ProcessedTerm -> [Int] -> IO [(Int, GameState)]- mkGameStates prog sizes = zip sizes <$> mapM (mkGameState (initRobot prog)) sizes-- toBenchmarks :: [(Int, GameState)] -> [Benchmark]- toBenchmarks gameStates =- [ bench (show n) $ whnfAppIO (runGame 1000) gs- | (n, gs) <- gameStates- ]
data/entities.yaml view
@@ -7,9 +7,22 @@ A tall, living entity made of a tough cellular material called "wood". They regrow after being harvested and are an important raw ingredient used in making many different devices.- properties: [portable, growable, opaque]+ properties: [portable, growable, opaque, combustible] growth: [500, 600]+ combustion:+ ignition: 0.01+ duration: [80, 120]+ product: ash +- name: ash+ display:+ attr: rock+ char: '#'+ description:+ - |+ Burned-out remnants of combustion.+ properties: [portable]+ - name: branch display: attr: wood@@ -25,7 +38,11 @@ char: 'l' description: - A wooden log, obtained by harvesting a tree and cutting off its branches.- properties: [portable]+ properties: [portable, combustible]+ combustion:+ ignition: 0.05+ duration: [40, 80]+ product: ash - name: board display:@@ -33,7 +50,11 @@ char: 'w' description: - A wooden board, made by cutting a log into pieces.- properties: [portable]+ properties: [portable, combustible]+ combustion:+ ignition: 0.2+ duration: [20, 40]+ product: ash - name: workbench display:@@ -57,7 +78,11 @@ description: - A flat material made of pressed and dried wood fibers, used as a surface on which to inscribe symbols.- properties: [portable]+ properties: [portable, combustible]+ combustion:+ ignition: 0.5+ duration: [10, 20]+ product: ash - name: PhD thesis display:@@ -336,8 +361,12 @@ - A plant with tufts of soft fibers that can be harvested and used to make things, including sheets of material that the local aliens like to drape over their bodies.- properties: [portable, growable]+ properties: [portable, growable, combustible] growth: [100, 800]+ combustion:+ ignition: 0.1+ duration: [20, 40]+ product: ash - name: linotype display:@@ -765,18 +794,16 @@ - 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- ```+ For example to move two units:+ - |+ `move; move;` - The `turn` command takes a direction as an argument, which can be either absolute (north, west, east, south) or relative (left, right, forward, back, down). - | Example:- ```- move; turn left; move; turn right- ```+ - |+ `move; turn left; move; turn right` capabilities: [move, turn] properties: [portable] @@ -1200,16 +1227,15 @@ description: - | 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`.+ 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- ```+ `d <- heading; turn east; move; turn d` properties: [portable] capabilities: [orient]
data/scenarios/Challenges/00-ORDER.txt view
@@ -7,10 +7,12 @@ word-search.yaml gopher.yaml ice-cream.yaml+combo-lock.yaml hanoi.yaml hackman.yaml lights-out.yaml bucket-brigade.yaml+wave.yaml wolf-goat-cabbage.yaml blender.yaml friend.yaml
data/scenarios/Challenges/2048.yaml view
@@ -5,10 +5,10 @@ objectives: - goal: - OK, OK, it's not really the same as the classic "2048" game.- However, your goal is still to make 2048! You start with a `1`+ However, your goal is still to make 2048! You start with a `1`{=entity} which regrows immediately when- it is harvested, so if you plant it, you can get as many as you want. Your- job is to combine `1`s in order to make a `2048`+ it is `harvest`ed, so if you plant it, you can get as many as you want. Your+ job is to combine `1`{=entity}s in order to make a `2048`{=entity} entity. - "Hint: the `format` command can turn numbers into strings!" condition: |
data/scenarios/Challenges/Mazes/easy_cave_maze.yaml view
@@ -7,7 +7,7 @@ - You are at the top of a cave that gradually descends until reaching a dead end. - At its bottom is a great treasure. - Luckily, the cave does not branch out, so it is easy to find the path to the treasure.- - Send a robot to the the item marked as '!'. You win once the robot grabs it.+ - Send a robot to the the item marked as '**!**'. You win once the robot `grab`s it. condition: | j <- robotNamed "judge"; as j {has "goal"}
data/scenarios/Challenges/Ranching/_gated-paddock/enclosure-checking.sw view
@@ -1,131 +1,11 @@-// 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.- return ();- } {- 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;- };+ return b; 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;+ maybePath <- path (inL ()) (inR "water");+ case maybePath (\_. return True) (\_. return False); end; def boolToInt = \b. if (b) {return 1} {return 0}; end;@@ -215,34 +95,12 @@ } 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;
data/scenarios/Challenges/Ranching/_gated-paddock/meandering-sheep.sw view
@@ -1,26 +1,7 @@ // 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;@@ -95,7 +76,7 @@ dist <- random 3; repeat dist ( - b <- isBlockedOrFenced;+ b <- blocked; if b {} { move; };
data/scenarios/Challenges/Ranching/capture.yaml view
@@ -15,7 +15,7 @@ goal: - | This porcine pest has been running amok.- Block the pig by a "monolith" on all four sides to capture it.+ Block the pig by a `monolith`{=entity} on all four sides to capture it. - Blocking on fewer than four sides will spook the pig, and he will escape! condition: | def isSurrounded = \n.
data/scenarios/Challenges/Ranching/gated-paddock.yaml view
@@ -12,143 +12,23 @@ 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.+ 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".+ Note that you can use the `drill` command (by way of the `post puller`{=entity} tool)+ to demolish a `fence`{=entity} that has been `place`d. 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.- return ();- } {- 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;- };+ return b; 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;+ maybePath <- path (inL ()) (inR "water");+ case maybePath (\_. return True) (\_. return False); end; def boolToInt = \b. if (b) {return 1} {return 0}; end;@@ -238,35 +118,13 @@ } 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; };@@ -287,7 +145,7 @@ 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+ You may want to add a `gate`{=entity} to the fence to give yourself easier access. condition: |- def getTruthForSheepIndex = \predicateCmd. \i.@@ -317,11 +175,12 @@ anySheep (has "clover") 3; prerequisite: enclose_sheep- - goal:+ - teaser: Knit sweater+ goal: - |- Yum! Contented, well-fed sheep may drop wool.+ Yum! Contented, well-fed sheep may drop `wool`{=entity}. - |- Winter is coming! Collect three wool bundles to make a sweater.+ Winter is coming! Collect three wool bundles to make a `sweater`{=entity}. - | Each sheep drops a finite amount over their lifetime.@@ -356,8 +215,8 @@ - [0, gate] - [10, hinge] - name: sheep- description:- - meandering livestock+ description: |+ meandering livestock display: invisible: false char: '@'@@ -365,6 +224,8 @@ dir: [0, 1] inventory: - [4, wool]+ unwalkable:+ - gate program: | run "scenarios/Challenges/Ranching/_gated-paddock/meandering-sheep.sw"; entities:
data/scenarios/Challenges/Ranching/powerset.yaml view
@@ -32,7 +32,7 @@ However, his experiment is incomplete! He has forgotten one combination. - | Place the missing hybrid combination in the empty eastern-most column.- After you have done this, `place` the "bell" anywhere, and then Bill will inspect+ After you have done this, `place` the `bell`{=entity} anywhere, and then Bill will inspect your work. prerequisite: not: wrong_anwser
data/scenarios/Challenges/Sliding Puzzles/3x3.yaml view
@@ -23,7 +23,7 @@ - | Or, if you prefer, `drill` a tile to cause it to slide into the adjacent empty space. However, you must not drill a tile that- has nowhere to slide. Also, drilling consumes "ink", which will be replenished+ has nowhere to slide. Also, drilling consumes `ink`{=entity}, which will be replenished after the sliding operation is complete, so avoid drilling too fast in succession. condition: |
data/scenarios/Challenges/Sokoban/Gadgets/no-reverse.yaml view
@@ -7,7 +7,7 @@ seed: 0 objectives: - goal:- - Grab the flower.+ - Grab the `flower`{=entity}. condition: | as base { has "flower";
data/scenarios/Challenges/Sokoban/Gadgets/one-way.yaml view
@@ -7,7 +7,7 @@ seed: 0 objectives: - goal:- - Grab the flower.+ - Grab the `flower`{=entity}. condition: | as base { has "flower";
data/scenarios/Challenges/Sokoban/Simple/trapdoor.yaml view
@@ -12,7 +12,7 @@ seed: 0 objectives: - goal:- - Place the flower on the target.+ - Place the `flower`{=entity} on the target. - You may have to start over if you get stuck. condition: | as base {
data/scenarios/Challenges/Sokoban/foresight.yaml view
@@ -13,7 +13,7 @@ fg: "#bbbbff" objectives: - goal:- - Push a monolith onto the base's initial location.+ - Push a `monolith`{=entity} onto the base's initial location. condition: | as base { teleport self (0,0);@@ -23,7 +23,7 @@ hidden: true optional: true goal:- - Grab the flower+ - Grab the `flower`{=entity} condition: | as base { has "flower";
+ data/scenarios/Challenges/_combo-lock/setup.sw view
@@ -0,0 +1,85 @@+def elif = \t. \then. \else. {if t then else} end+def else = \t. t end+def doN = \n. \f. if (n > 0) {f; doN (n - 1) f} {}; end;++def colorFromIndex = \i.+ if (i == 0) {"R"}+ $ elif (i == 1) {"G"}+ $ else {"B"};+ end;++def pixelFromColor = \c.+ "dial (" ++ c ++ ")";+ end;++def checkCombo = \noMismatchYet. \stepsTaken. \colorString.++ let remainingCount = chars colorString in+ if (remainingCount > 0) {++ let splitted = split 1 colorString in+ let nextLetter = fst splitted in+ let remainingLetters = snd splitted in+ let expectedPixel = pixelFromColor nextLetter in++ move;+ isExpectedHere <- ishere expectedPixel;+ checkCombo (isExpectedHere && noMismatchYet) (stepsTaken + 1) remainingLetters;+ } {+ turn back;++ // Replace the cell watches+ doN stepsTaken (watch down; move);+ turn back;+ return noMismatchYet;+ };+ end;++def unlockGate = \n.+ move;+ turn right;+ move;+ turn left;+ doN n (grab; move);+ return ()+ end;++def doUntilCorrect = \colorString.+ isCorrect <- checkCombo true 0 colorString;+ if isCorrect {+ let remainingCount = chars colorString in+ unlockGate remainingCount;+ return true;+ } {+ wait 1000;+ doUntilCorrect colorString;+ };+ end;+ +def createCombo = \colorString.+ // Scenario map starts with red pixels to+ // mark the combo sequence+ redPixelHere <- ishere $ pixelFromColor "R";++ if redPixelHere {+ r <- random 3;+ let newColor = colorFromIndex r in+ watch down;+ move;+ createCombo $ newColor ++ colorString;+ } {+ turn back;+ return colorString;+ };+ end;++def go =+ comboString <- instant ( + move;+ createCombo "";+ );+ // say comboString;+ instant $ doUntilCorrect comboString;+ end;++go;
+ data/scenarios/Challenges/_combo-lock/solution.sw view
@@ -0,0 +1,65 @@+def moveToLock =+ emptyHere <- isempty;+ if emptyHere {move; moveToLock} {};+ end;++def cycleCombos = \n.+ wait 1;+ entityNorth <- scan north;+ let hasGate = case entityNorth (\_. false) (\x. x == "gate") in+ if hasGate {+ if (n > 0) {+ drill down;+ maybeNextEnt <- scan east;+ case maybeNextEnt return (\_. turn east; move; cycleCombos 3);+ cycleCombos $ n - 1;+ } {+ turn west;+ move;+ };+ } {}+ end;++def moveUntilBlocked =+ isblocked <- blocked;+ if isblocked {} {+ move;+ moveUntilBlocked;+ };+ end;++def toLeftEdge =+ turn north;+ move;+ turn left;+ moveUntilBlocked;+ turn north;+ end;++def goUp =+ toLeftEdge;+ move; move; move;+ end;++def grabBitcoin =+ move; move;+ turn right;+ move; move;+ grab;+ end;++def go =+ moveToLock;+ cycleCombos 3;+ goUp;+ cycleCombos 3;+ goUp;+ cycleCombos 3;+ goUp;+ cycleCombos 3;++ toLeftEdge;+ grabBitcoin;+ end;++go;
+ data/scenarios/Challenges/_wave/solution.sw view
@@ -0,0 +1,15 @@+def go =+ move;+ go;+ end;++def start =+ turn right;+ wait 5;+ try {+ go;+ } {};+ grab;+ end;++start;
data/scenarios/Challenges/arbitrage.yaml view
@@ -16,7 +16,7 @@ As an itinerant merchant, you may exploit market asymmetry for profit. - |- Amass a fortune of 100 paperclips.+ Amass a fortune of 100 `paperclip`{=entity}s. condition: | as base { pcount <- count "paperclip";
data/scenarios/Challenges/blender.yaml view
@@ -8,10 +8,10 @@ - teaser: Get amulet goal: - |- `grab` the Amulet of Yoneda from the northwest sanctum while- timing your passage carefully to avoid Side Effects (X) on patrol.+ `grab` the `Amulet of Yoneda`{=entity} from the northwest sanctum while+ timing your passage carefully to avoid Side Effects (**X**) on patrol. - |- To unlock a red door, `drill` it with the "door key" equipped.+ To unlock a red door, `drill` it with the `door key`{=entity} equipped. condition: | as base {has "Amulet of Yoneda"} prerequisite:
data/scenarios/Challenges/bridge-building.yaml view
@@ -55,7 +55,7 @@ - id: hammer_time teaser: Hammer time goal:- - Produce an obsidian shard.+ - Produce an `obsidian shard`{=entity}. optional: true hidden: true condition: |@@ -87,14 +87,14 @@ - id: get_map teaser: Get the map goal:- - As a humble peat farmer, you subsist in a simple cabin by the bog.+ - As a humble `peat`{=entity} farmer, you subsist in a simple cabin by the `bog`{=entity}. Though long content with this ascetic lifestyle, recently the barren walls have left you restless. Something is missing... - "The majestic landscape that is your back yard is insufficient to distract you---not- even the ferocious, lava-spewing volcano little more than a stone's throw from+ even the ferocious, `lava`{=entity}-spewing volcano little more than a stone's throw from your porch. You are preoccupied by one task: to find the perfect household decoration."- - First, grab a map to orient yourself.+ - First, grab a `map`{=entity} to orient yourself. condition: | as base { has "map";@@ -103,18 +103,18 @@ teaser: Find the temple prerequisite: get_map goal:- - You study the map.- - Glacier-bound mountains tower in the east,- a volcano oozes a river of lava to the north, and beyond that lies a mountain lake, punctuated with islands+ - You study the `map`{=entity}.+ - Glacier-bound mountains tower in the `east`,+ a volcano oozes a river of `lava`{=entity} to the `north`, and beyond that lies a mountain lake, punctuated with islands in the northwest. Iron mines penetrate the base of the volcano. They could be useful, but how will you get there?- - A jungle abuts the volcano, ensconcing an ancient ruin.+ - A `jungle`{=entity} abuts the volcano, ensconcing an ancient ruin. The map notes that bygone travelers have stashed tools among the northeasterly mountains to blaze a path through the jungle.- - Your only neighbor, a hemp farmer to the northwest, has evacuated since the sudden volcanic eruption.- - A disused quarry and clay pit flanks your cabin to the west, as does the familiar, swampy bog to the east.- A highly-prized flower is said to grow in the caves beyond the bog.- - Your mind is made up. You will pillage the ruins for treasure! Head to the ruins and "scan" them.+ - Your only neighbor, a `hemp`{=entity} farmer to the northwest, has evacuated since the sudden volcanic eruption.+ - A disused quarry and clay pit flanks your cabin to the `west`, as does the familiar, swampy `bog`{=entity} to the `east`.+ A highly-prized `flower`{=entity} is said to grow in the caves beyond the bog.+ - Your mind is made up. You will pillage the ruins for treasure! Head to the ruins and `scan` them. Ingenuity and endurance are your allies as you forge paths through varied obstacles. Study your "recipes" for clues! condition: |@@ -126,8 +126,8 @@ prerequisite: find_temple goal: - "A note on the door says:"- - '"Greetings, intrepid traveler. Encircle this temple with the rare "flower" of the southeastern- caves, and the treasure of this temple shall be revealed."'+ - '"Greetings, intrepid traveler. Encircle this temple with the rare `flower`{=entity} of the southeastern+ caves, and the treasure of this `temple`{=entity} shall be revealed."' - Plant a ring of flowers around the jungle temple. condition: | as base {
data/scenarios/Challenges/bucket-brigade.yaml view
@@ -57,14 +57,14 @@ - id: deliver_coal_lump teaser: Get coal to base goal:- - Deliver a "coal lump" to the base.+ - Deliver a `coal lump`{=entity} to the base. - |- To excavate coal from the "lignite mine" (M), a robot needs to- `drill` while in posession of a "bucketwheel excavator".+ To excavate coal from the `lignite mine`{=entity} (**M**), a robot needs to+ `drill` while in possession of a `bucketwheel excavator`{=entity}. - | To assemble the excavator, you'll need to repurpose- some "treads".- Unfortunately, you have only one set of "treads".+ some `treads`{=entity}.+ Unfortunately, you have only one set of `treads`{=entity}. You'll have to make do... condition: | as base {has "coal lump"}
data/scenarios/Challenges/chess_horse.yaml view
@@ -4,8 +4,8 @@ description: In this quirky challenge, you move as the chess knight piece. Can you capture the enemy king? objectives: - goal:- - Robots can use the 'move' command to move.- But they only 'turn' in cardinal directions. + - Robots can use the `move` command to move.+ But they only `turn` in cardinal directions. - You are special. You are a knight. - Go forth and capture the King! condition: |
+ data/scenarios/Challenges/combo-lock.yaml view
@@ -0,0 +1,135 @@+version: 1+name: Combination locks+author: Karl Ostmo+description: |+ Unlock the gates+creative: false+seed: 4+objectives:+ - goal:+ - |+ Several combination-locked gates lie between you+ and treasure.+ - |+ Each "dial" can be one of three colors.+ `drill` a dial to cycle to its next color.+ The adjacent `gate`{=entity} opens when the correct+ combination is set.+ - |+ Unlock all of the combination locks and `grab`+ the `bitcoin`{=entity}.+ condition: |+ as base {has "bitcoin"};+robots:+ - name: base+ dir: [1, 0]+ display:+ invisible: false+ devices:+ - ADT calculator+ - branch predictor+ - hourglass+ - comparator+ - compass+ - dictionary+ - drill+ - grabber+ - lambda+ - lodestone+ - logger+ - keyboard+ - net+ - scanner+ - strange loop+ - treads+ - name: lockbot+ system: true+ dir: [1, 0]+ display:+ invisible: true+ program: |+ run "scenarios/Challenges/_combo-lock/setup.sw"+solution: |+ run "scenarios/Challenges/_combo-lock/solution.sw"+entities:+ - name: gate+ display:+ char: '#'+ description:+ - A locked door+ properties: [known, unwalkable]+ - name: "dial (R)"+ display:+ char: '•'+ attr: red+ description:+ - A red dial+ properties: [known]+ - name: "dial (G)"+ display:+ char: '•'+ attr: green+ description:+ - A green dial+ properties: [known]+ - name: "dial (B)"+ display:+ char: '•'+ attr: blue+ description:+ - A blue dial+ properties: [known]+recipes:+ - in:+ - [1, "dial (R)"]+ out:+ - [1, "dial (G)"]+ required:+ - [1, drill]+ time: 0+ - in:+ - [1, "dial (G)"]+ out:+ - [1, "dial (B)"]+ required:+ - [1, drill]+ time: 0+ - in:+ - [1, "dial (B)"]+ out:+ - [1, "dial (R)"]+ required:+ - [1, drill]+ time: 0+known: [boulder, water, bitcoin]+world:+ dsl: |+ {water}+ upperleft: [-1, -1]+ offset: false+ palette:+ '.': [grass, erase]+ '$': [grass, bitcoin]+ '@': [grass, boulder]+ '#': [grass, gate]+ 'B': [grass, erase, base]+ 'c': [grass, dial (R)]+ 'a': [grass, erase, lockbot]+ map: |+ .......$......+ @@@@@......@@@+ ....@######@..+ ....acccccc...+ ..............+ @@@@@.....@@@@+ ....@#####@...+ ....accccc....+ ..............+ @@@@@....@@@@@+ ....@####@....+ ....acccc.....+ ..............+ @@@@@...@@@@@@+ ....@###@.....+ ..B.accc......+ ..............
data/scenarios/Challenges/friend.yaml view
@@ -88,6 +88,8 @@ '*': [grass, flower] '@': [stone, boulder] upperleft: [-20, 2]+ dsl: |+ "classic" map: |- c,..,,,,,,..,,,,...,. ,..,,,,,,,...........
data/scenarios/Challenges/gopher.yaml view
@@ -10,12 +10,12 @@ teaser: Defeat gopher goal: - |- A gopher (G) is defiling your immaculate garden!+ A gopher (**G**) is defiling your immaculate garden! - |- He will burrow (o) underground awhile, then pop up+ He will burrow (**o**) underground awhile, then pop up anywhere within the rectangular grassy region- to gloat atop his "mound" of dirt for a short time.- `drill` the "mound" while he sits to drive him+ to gloat atop his `mound`{=entity} of dirt for a short time.+ `drill` the `mound`{=entity} while he sits to drive him away. Eventually you should wear down his resolve! condition: | try {@@ -129,12 +129,14 @@ run "scenarios/Challenges/_gopher/solution.sw" known: [] world:+ dsl: |+ "classic" upperleft: [-1, 1] offset: false palette:- 'x': [dirt]- '.': [grass]- 'g': [dirt, null, gopher]+ 'x': [dirt, erase]+ '.': [grass, erase]+ 'g': [dirt, erase, gopher] map: | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx x............................x
data/scenarios/Challenges/hackman.yaml view
@@ -19,10 +19,10 @@ - teaser: Get pellets goal: - |- Pick up all of the caffeine "pellets" so that Hackman can write more code.+ Pick up all of the caffeine `pellet`{=entity}s so that Hackman can write more code. - | Pay no mind to the colorful ghosts meandering about. That is, unless- you are feeling generous with your "donuts"...+ you are feeling generous with your `donut`{=entity}s... - | Can you find all of the secret objectives? condition: |
data/scenarios/Challenges/ice-cream.yaml view
@@ -11,15 +11,15 @@ goal: - | Congratulations on the grand opening of your new ice cream shop.- You have advertised: "All you can eat, for 1 bitcoin!"+ You have advertised: "All you can eat, for 1 `bitcoin`{=entity}!" - |- A customer is approaching the "Counter". They look hungry!+ A customer is approaching the `Counter`{=entity}. 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".+ Oh dear, you've forgotten to provision your shop with a `calculator`{=entity}. Let's hope this order is simple. condition: | customer <- robotnamed "customer";
data/scenarios/Challenges/maypole.yaml view
@@ -19,7 +19,7 @@ teaser: Around you go! goal: - |- Go around the maypole several times counter-clockwise.+ Go around the `maypole`{=entity} several times counter-clockwise. condition: | monitor <- robotnamed "monitor"; as monitor {has "dizzy"};
data/scenarios/Challenges/teleport.yaml view
@@ -4,7 +4,7 @@ description: An impossible challenge - can you magically jump across the water? objectives: - goal:- - Get to the other room and grab the lambda.+ - Get to the other room and grab the `lambda`{=entity}. - Oh wait. - The tunnel is flooded. - Just give up then. It is impossible to get there.
+ data/scenarios/Challenges/wave.yaml view
@@ -0,0 +1,89 @@+version: 1+name: Wave+author: Karl Ostmo+description: |+ Ride the wave+creative: false+objectives:+ - goal:+ - |+ Grab the `bitcoin`{=entity} at the east end of the path.+ Don't let the patrolling robots catch you!+ prerequisite:+ not: got_caught+ condition: |+ as base {has "bitcoin"};+ - id: got_caught+ teaser: Got caught+ optional: true+ hidden: true+ goal:+ - |+ The robots caught you!+ condition: |+ as base {x <- meet; return $ case x (\_. false) (\_. true)};+robots:+ - name: base+ dir: [0, 1]+ devices:+ - branch predictor+ - comparator+ - dictionary+ - grabber+ - hourglass+ - clock+ - lambda+ - logger+ - net+ - scanner+ - strange loop+ - treads+ - name: wavebot+ system: true+ dir: [0, 1]+ display:+ invisible: false+ attr: 'plant'+ program: |+ def crossPath =+ move; move; move; move; move; move;+ turn back;+ wait 5;+ end;+ def go =+ crossPath;+ go;+ end;+ def start =+ pos <- whereami;+ wait $ fst pos;+ go;+ end;+ start;+solution: |+ run "scenarios/Challenges/_wave/solution.sw"+entities: []+known: [wavy water, water, bitcoin]+world:+ dsl: |+ overlay+ [ {dirt, water}+ , if (x + y / 2) % 5 == 0 then {dirt, wavy water} else {blank}+ ]+ upperleft: [-3, 6]+ offset: false+ palette:+ 'B': [grass, erase, base]+ 'w': [grass, erase, wavebot]+ 'x': [dirt, water]+ 'z': [grass, bitcoin]+ '.': [grass, erase]+ map: |+ xxxxxx..................................................................................................................................................................................................................................................xxxxxx+ xxx........................................................................................................................................................................................................................................................xxx+ x............................................................................................................................................................................................................................................................x+ B............................................................................................................................................................................................................................................................z+ x............................................................................................................................................................................................................................................................x+ xxx........................................................................................................................................................................................................................................................xxx+ xxxxxxwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwxxxxxx+
data/scenarios/Challenges/wolf-goat-cabbage.yaml view
@@ -40,11 +40,11 @@ objectives: - goal: - |- Ferry all three of the wolf, goat, and cabbage across the lake.+ Ferry all three of the `wolf`{=entity}, `goat`{=entity}, and `cabbage`{=entity} 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.+ Furthermore, if left unattended together, the `wolf`{=entity} will eat the `goat`{=entity},+ or the `goat`{=entity} will eat the `cabbage`{=entity}. condition: | run "data/scenarios/Challenges/_wolf-goat-cabbage/together-on-east-bank.sw"; prerequisite:
data/scenarios/Challenges/word-search.yaml view
@@ -7,9 +7,9 @@ creative: false objectives: - goal:- - Use the `drill` command (e.g. "drill down" when on top of the+ - 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.+ spells `C`{=entity}`O`{=entity}`W`{=entity} within the designated playfield. - | The sequence may appear horizontally in either the leftward or rightward direction,
data/scenarios/Speedruns/curry.yaml view
@@ -1,7 +1,7 @@ version: 1 name: Curry author: Brent Yorgey-description: Race to make a bowl of curry as quickly as possible.+description: Race to make a bowl of `curry`{=entity} as quickly as possible. See the Swarm wiki for more information on Swarm speedrunning. objectives: - goal:
data/scenarios/Speedruns/forester.yaml view
@@ -1,11 +1,11 @@ version: 1 name: Forester author: Brent Yorgey-description: Race to harvest 1024 trees as quickly as possible.+description: Race to harvest 1024 `tree`{=entity}s as quickly as possible. See the Swarm wiki for more information on Swarm speedrunning. objectives: - goal:- - Harvest 1024 trees as quickly as possible!+ - Harvest 1024 `tree`{=entity}s as quickly as possible! condition: as base {n <- count "tree"; return (n >= 1024)} robots: - name: base
data/scenarios/Speedruns/mithril.yaml view
@@ -1,11 +1,11 @@ version: 1 name: Mithril author: Brent Yorgey-description: Race to mine some mithril.+description: Race to mine some `mithril`{=entity}. See the Swarm wiki for more information on Swarm speedrunning. objectives: - goal:- - Mine some mithril as quickly as possible!+ - Mine some `mithril`{=entity} as quickly as possible! condition: as base {has "mithril"} robots: - name: base
data/scenarios/Testing/00-ORDER.txt view
@@ -1,3 +1,4 @@+Achievements 373-drill.yaml 394-build-drill.yaml 428-drowning-destroy.yaml@@ -40,5 +41,11 @@ 1320-world-DSL 1356-portals 144-subworlds+836-pathfinding+1341-command-count.yaml+1355-combustion.yaml 1379-single-world-portal-reorientation.yaml 1399-backup-command.yaml+1430-built-robot-ownership.yaml+1536-custom-unwalkable-entities.yaml+1535-ping
+ data/scenarios/Testing/1341-command-count.yaml view
@@ -0,0 +1,107 @@+version: 1+name: Count commands+creative: true+description: |+ Count commands and demonstrate various "duty cycles"+ with system robots. The four robots should have duty+ cycles of 100%, 50%, 33%, and 25% based on the number+ of ticks that they `wait`.+objectives:+ - goal:+ - |+ `grab` the "tree".+ condition: |+ as base {has "tree"}+solution: |+ move;+ move;+ wait 10;+ x <- harvest;+ if (x == "flower") {+ turn right;+ move;+ grab;+ } {+ return "";+ };+robots:+ - name: base+ dir: [1, 0]+ devices:+ - treads+ - grabber+ - harvester+ - logger+ - comparator+ - hourglass+ - branch predictor+ - name: idler1+ dir: [1, 0]+ system: true+ devices:+ - dictionary+ - strange loop+ - hourglass+ program: |+ def go =+ wait 1;+ go;+ end;+ go;+ - name: idler2+ dir: [1, 0]+ system: true+ devices:+ - dictionary+ - strange loop+ - hourglass+ program: |+ def go =+ wait 2;+ go;+ end;+ go;+ - name: idler3+ dir: [1, 0]+ system: true+ devices:+ - dictionary+ - strange loop+ - hourglass+ program: |+ def go =+ wait 3;+ go;+ end;+ go;+ - name: idler4+ dir: [1, 0]+ system: true+ devices:+ - dictionary+ - strange loop+ - hourglass+ program: |+ def go =+ wait 4;+ go;+ end;+ go;+known: [flower, tree]+world:+ default: [blank]+ palette:+ '.': [grass]+ 'f': [grass, flower]+ 'T': [grass, tree]+ 'B': [grass, null, base]+ '1': [grass, null, idler1]+ '2': [grass, null, idler2]+ '3': [grass, null, idler3]+ '4': [grass, null, idler4]+ upperleft: [-1, 1]+ map: |+ ........1+ .B.fff..2+ ...T....3+ ........4
+ data/scenarios/Testing/1355-combustion.yaml view
@@ -0,0 +1,133 @@+version: 1+name: Combustion+creative: false+seed: 0+description: Demo of spreading fire+objectives:+ - goal:+ - Blow up the dynamite+ condition: |+ j <- robotnamed "judge";+ as j {+ ishere "crater";+ };+solution: |+ def doN = \n. \f. if (n > 0) {f; doN (n - 1) f} {}; end;++ move;+ ignite forward;+ turn right;+ move;+ ignite forward;+ turn right;+ move; move; move;+ turn right;+ move;+ ignite left;+ move;+ turn right;+ doN 2 (move; ignite left);+ doN 14 move;+ turn left;+ doN 5 move;+ turn left;+ ignite right;+ doN 8 move;+ ignite right;+ doN 10 move;+ ignite right;+ doN 8 move;+ ignite right;+robots:+ - name: base+ dir: [1, 0]+ devices:+ - branch predictor+ - calculator+ - dictionary+ - comparator+ - logger+ - treads+ - torch+ - name: judge+ dir: [1, 0]+ system: true+ display:+ invisible: true+ char: J+entities:+- name: torch+ display:+ attr: wood+ char: 't'+ description:+ - Can set things on fire+ properties: [known, portable]+ capabilities: [ignite]+- name: fuse+ display:+ attr: wood+ char: '~'+ description:+ - Reliably combustible+ combustion:+ ignition: 1+ duration: [8, 8]+ product: null+ properties: [known, portable, combustible]+- name: dynamite+ display:+ attr: red+ char: '!'+ description:+ - Explosive material+ combustion:+ ignition: 1+ duration: [2, 2]+ product: crater+ properties: [known, portable, combustible]+- name: crater+ display:+ attr: rock+ char: '@'+ description:+ - Result of explosive excavation+ properties: [known]+known: [ash, tree, log, board, paper, cotton]+world:+ palette:+ 'Ω': [grass, null, base]+ 'T': [grass, tree]+ 'q': [grass, paper]+ 'l': [grass, log]+ 'b': [grass, board]+ 'i': [grass, cotton]+ 'F': [grass, fuse]+ 'd': [grass, dynamite, judge]+ '.': [grass]+ upperleft: [0, 0]+ map: |+ ..iiii....bbbb..TT..llll....iiii.....+ ..iiii....bbbb..TT..llll....iiii.....+ ..iiii....bbbb..TT..llll....iiii.....+ ..iiii....bbbb..TT..llll....iiii.....+ ..iiii....bbbb..TT..llll....iiii.....+ ..iiii....bbbb..TT..llll....iiii.....+ ..TTTT....TTTT..TT..TTTT....TTTT.....+ ..TTTT....TTTT..TT..TTTT....TTTT.....+ ................TT...................+ ................TT...................+ iiiiiiiiiii.....TT....FFFFF...FFFFF..+ ......iiiiii....TT....F...F...F...F..+ iiiiiiiiiiiii...TT....F...F...F...F..+ ......iiiiiiii........F...F...F...F..+ iiiiiiiiiiiiiii..Ω.FFFF...FFFFF...d..+ ......iiiiiiii.......................+ iiiiiiiiiiiii.....qqqqqqqqqqqqqqqq...+ ......iiiiii......qqqqqqqqqqqqqqqq...+ iiiiiiiiiii.......qqqqqqqqqqqqqqqq...+ ..................qqqqqqqqqqqqqqqq...+ ..................qqqqqqqqqqqqqqqq...+ ..................qqqqqqqqqqqqqqqq...+ ..................qqqqqqqqqqqqqqqq...+ ..................qqqqqqqqqqqqqqqq...
+ data/scenarios/Testing/1430-built-robot-ownership.yaml view
@@ -0,0 +1,64 @@+version: 1+name: Ownership of system-built robots+creative: true+description: Demo of system robot construction+robots:+ - name: base+ dir: [1, 0]+ display:+ char: Ω+ attr: robot+ inventory:+ - [5, logger]+ - [5, compass]+ - [5, solar panel]+ - [5, treads]+ - [5, grabber]+ devices:+ - logger+ - 3D printer+ - dictionary+ - grabber+ - hearing aid+ - name: sysbot+ dir: [-1, 0]+ system: true+ display:+ char: j+ attr: robot+ invisible: true+ devices:+ - treads+ - solar panel+ - logger+ - 3D printer+ inventory:+ - [1, solar panel]+ - [1, logger]+ - [1, treads]+ - [1, string]+ program: |+ build {move; say (format parent)}+solution: |+ listen; build {move; say (format parent)}+objectives:+ - goal:+ - Check that system robots build system robots and normal robots do not.+ - This check is performed in integration tests.+ condition: |+ r2 <- robotNumbered 2;+ r3 <- robotNumbered 3;+ return true+known: []+world:+ palette:+ 'Ω': [grass, null, base]+ 'r': [stone, null, sysbot]+ '.': [grass]+ upperleft: [0, 0]+ map: |+ .........+ .........+ ...Ω..r..+ .........+ .........
+ data/scenarios/Testing/1535-ping/00-ORDER.txt view
@@ -0,0 +1,2 @@+1535-in-range.yaml+1535-out-of-range.yaml
+ data/scenarios/Testing/1535-ping/1535-in-range.yaml view
@@ -0,0 +1,87 @@+version: 1+name: Ping command - Demo+description: |+ Robot is in range for ping+creative: false+objectives:+ - teaser: Follow buddy+ goal:+ - You and your buddy each have half of a map to a cache of buried treasure.+ - |+ `give` him your `map piece`{=entity}, which he will use to+ locate the `bitcoin`{=entity}, which you must `grab`.+ condition: |+ as base {+ has "bitcoin";+ }+solution: |+ run "scenarios/Testing/1535-ping/_1535-in-range/solution.sw"+entities:+ - name: transponder+ display:+ char: 'x'+ description:+ - Enables `ping` command+ properties: [known, portable]+ capabilities: [ping]+ - name: map piece+ display:+ char: 'm'+ description:+ - Half of a treasure map+ properties: [known, portable]+robots:+ - name: base+ dir: [1,0]+ devices:+ - ADT calculator+ - antenna+ - branch predictor+ - comparator+ - compass+ - dictionary+ - grabber+ - hourglass+ - logger+ - transponder+ - treads+ inventory:+ - [1, map piece]+ - name: buddy+ dir: [-1, 0]+ system: true+ display:+ invisible: false+ devices:+ - ADT calculator+ - antenna+ - bitcoin+ - branch predictor+ - comparator+ - counter+ - dictionary+ - grabber+ - hourglass+ - logger+ - transponder+ - treads+ inventory:+ - [1, map piece]+ - [1, bitcoin]+ program: |+ run "scenarios/Testing/1535-ping/_1535-in-range/buddy.sw"+known: [bitcoin]+world:+ dsl: |+ overlay+ [ {terrain: stone}+ , if (x/5 + y/5) % 2 == 0 then {terrain: dirt} else {blank}+ , if ((x + 3) % 19)/12 + (y % 19)/12 == 0 then {terrain: grass} else {blank}+ ]+ palette:+ 'B': [blank, null, base]+ 'r': [blank, null, buddy]+ '.': [blank]+ upperleft: [-1, 0]+ map: |+ B.r
+ data/scenarios/Testing/1535-ping/1535-out-of-range.yaml view
@@ -0,0 +1,67 @@+version: 1+name: Ping command - Range limits+description: |+ Demo effect of antenna on ping range+creative: false+objectives:+ - teaser: Escape+ goal:+ - Get out of `ping` range of your buddy's `transponder`{=entity}+ condition: |+ r <- robotnamed "buddy";+ as r {+ response <- ping base;+ return $ case response (\_. true) (\_. false);+ }+solution: |+ run "scenarios/Testing/1535-ping/_1535-out-of-range/solution.sw"+entities:+ - name: transponder+ display:+ char: 'x'+ description:+ - Enables `ping` command+ properties: [known, portable]+ capabilities: [ping]+robots:+ - name: base+ dir: [-1,0]+ devices:+ - calculator+ - antenna+ - branch predictor+ - comparator+ - dictionary+ - grabber+ - hourglass+ - logger+ - transponder+ - welder+ - name: buddy+ dir: [1, 0]+ devices:+ - ADT calculator+ - grabber+ - hourglass+ - logger+ - transponder+ inventory:+ - [1, treads]+ program:+ give base "treads";+known: []+world:+ dsl: |+ overlay+ [ {terrain: blank}+ , if (x/4 + y/4) % 2 == 0 then {terrain: dirt} else {blank}+ , if ((x + 3) % 19)/12 + (y % 19)/12 == 0 then {terrain: grass} else {blank}+ ]+ palette:+ 'B': [stone, null, base]+ 'r': [blank, null, buddy]+ '.': [ice]+ 'x': [stone]+ upperleft: [0, 0]+ map: |+ rB.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x
+ data/scenarios/Testing/1535-ping/_1535-in-range/buddy.sw view
@@ -0,0 +1,41 @@+def doN = \n. \f. if (n > 0) {f; doN (n - 1) f} {}; end;++def waitForMap =+ mapPieceCount <- count "map piece";+ if (mapPieceCount < 2) {+ wait 1;+ waitForMap;+ } {};+ end;++def randomReverse =+ x <- random 2;+ if (x == 0) {+ turn back;+ } {}+ end;++def goToTreasure = \dirMin. \dirMax.+ let randAmplitude = dirMax - dirMin in++ xRand <- random randAmplitude;+ let xDist = dirMin + xRand in+ randomReverse;+ doN xDist move;++ turn left;++ yRand <- random randAmplitude;+ let yDist = dirMin + yRand in+ randomReverse;+ doN yDist move;++ place "bitcoin";+ end;++def go =+ waitForMap;+ goToTreasure 10 40;+ end;++go;
+ data/scenarios/Testing/1535-ping/_1535-in-range/solution.sw view
@@ -0,0 +1,57 @@+def doN = \n. \f. if (n > 0) {f; doN (n - 1) f} {}; end;++def goToBuddy = \loc.++ // log $ format loc;+ + let longitudinalDist = snd loc in+ absFwd <- if (longitudinalDist < 0) {+ turn back;+ return $ -longitudinalDist;+ } {+ return longitudinalDist;+ };+ doN absFwd move;+ if (longitudinalDist < 0) {+ turn back;+ } {};++ let lateralDist = fst loc in+ absSide <- if (lateralDist < 0) {+ turn left;+ return $ -lateralDist;+ } {+ turn right;+ return lateralDist;+ };+ doN absSide move;+ end;++def checkNeedToMove = \f. \loc.+ wait 3;+ if (loc == (0, 0)) {+ return ()+ } {+ goToBuddy loc;+ f;+ }+ end;++def pingLoop = \buddy.+ maybeLoc <- ping buddy;+ case maybeLoc return $ checkNeedToMove $ pingLoop buddy;+ end;++def giveToBuddy = \buddy.+ give buddy "map piece";+ pingLoop buddy;+ end;++def go =+ move;+ maybeBuddy <- meet;+ case maybeBuddy return giveToBuddy;+ grab;+ end;++go;
+ data/scenarios/Testing/1535-ping/_1535-out-of-range/solution.sw view
@@ -0,0 +1,11 @@+def doN = \n. \f. if (n > 0) {f; doN (n - 1) f} {}; end;++def go =+ wait 2;+ equip "treads";+ turn back;+ doN 64 move;+ unequip "antenna";+ end;++go;
+ data/scenarios/Testing/1536-custom-unwalkable-entities.yaml view
@@ -0,0 +1,50 @@+version: 1+name: Custom unwalkability+description: The base robot cannot walk through trees.+ The scenario shall be failed if the robot+ manages to walk through the tree by moving+ three cells to the east.+objectives:+ - goal:+ - Get the flower+ condition: |+ as base {has "flower"};+ prerequisite:+ not: has_bitcoin+ - id: has_bitcoin+ optional: true+ goal:+ - Do not get the bitcoin+ condition: |+ as base {has "bitcoin"};+solution: |+ def tryMove = try {move} {}; end;+ tryMove;+ tryMove;+ tryMove;+ grab;+robots:+ - name: base+ dir: [1, 0]+ display:+ attr: robot+ devices:+ - logger+ - grabber+ - treads+ - dictionary+ - net+ unwalkable:+ - tree+known: [tree, flower, bitcoin]+world:+ palette:+ 'B': [grass, null, base]+ '.': [grass]+ 'T': [grass, tree]+ 'b': [grass, bitcoin]+ 'f': [grass, flower]+ upperleft: [0, 0]+ map: |+ BfTb+
+ data/scenarios/Testing/836-pathfinding/00-ORDER.txt view
@@ -0,0 +1,8 @@+836-path-exists-find-location.yaml+836-path-exists-find-entity.yaml+836-path-exists-find-entity-unwalkable.yaml+836-path-exists-distance-limit-unreachable.yaml+836-path-exists-distance-limit-reachable.yaml+836-no-path-exists1.yaml+836-no-path-exists2.yaml+836-automatic-waypoint-navigation.yaml
@@ -0,0 +1,154 @@+version: 1+name: Automatic navigation between waypoints+description: |+ Demonstrate shortest-path patrolling between waypoints+creative: false+solution: |+ run "scenarios/Testing/836-pathfinding/_836-automatic-waypoint-navigation/patrol.sw"+objectives:+ - goal:+ - Collect 64 eggs+ condition: |+ as base {+ eggCount <- count "egg";+ return $ eggCount >= 64;+ };+attrs:+ - name: easter_egg+ fg: "#ffff88"+ bg: "#eebbff"+entities:+ - name: wayfinder+ display:+ char: 'w'+ description:+ - Enables `path` command+ properties: [known, portable]+ capabilities: [path]+ - name: atlas+ display:+ char: 'a'+ description:+ - Enables `waypoint` command+ properties: [known, portable]+ capabilities: [waypoint]+ - name: kudzu+ display:+ char: 'k'+ attr: plant+ description:+ - Dense, impassable plant.+ properties: [known, unwalkable, growable]+ growth: [30, 50]+ - name: egg+ display:+ char: 'o'+ attr: easter_egg+ description:+ - A colorful egg laid by the rabbit+ properties: [known, portable]+ growth: [5, 10]+robots:+ - name: base+ loc: [0, 0]+ dir: [1, 0]+ devices:+ - ADT calculator+ - atlas+ - branch predictor+ - comparator+ - compass+ - dictionary+ - grabber+ - logger+ - net+ - scanner+ - treads+ - wayfinder+ - name: gardener+ description: Periodically chops down the kudzu plant+ dir: [1, 0]+ system: true+ devices:+ - ADT calculator+ - branch predictor+ - comparator+ - compass+ - dictionary+ - harvester+ - treads+ - logger+ display:+ invisible: true+ program: |+ run "scenarios/Testing/836-pathfinding/_836-automatic-waypoint-navigation/gardener.sw"+ - name: rabbit+ dir: [1, 0]+ loc: [2, -2]+ system: true+ devices:+ - treads+ - logger+ inventory:+ - [64, egg]+ display:+ invisible: false+ attr: snow+ char: R+ program: |+ run "scenarios/Testing/836-pathfinding/_836-automatic-waypoint-navigation/rabbit.sw"+known: [flower, boulder, kudzu]+world:+ upperleft: [-1, 1]+ palette:+ '.': [grass]+ 'k': [stone, kudzu, gardener]+ '┌': [stone, upper left corner]+ '┐': [stone, upper right corner]+ '└': [stone, lower left corner]+ '┘': [stone, lower right corner]+ '─': [stone, horizontal wall]+ '│': [stone, vertical wall]+ structures:+ - name: cornerbox+ structure:+ palette:+ '.': [dirt]+ '@': [dirt, boulder]+ 'w':+ cell: [dirt]+ waypoint:+ name: wp+ map: |+ @@@+ @w.+ @.@+ placements:+ - src: cornerbox+ offset: [2, -2]+ orient:+ up: north+ - src: cornerbox+ offset: [8, -2]+ orient:+ up: east+ - src: cornerbox+ offset: [8, -6]+ orient:+ up: south+ - src: cornerbox+ offset: [2, -6]+ orient:+ up: west+ map: |+ ┌───────────┐+ │...........│+ │...........│+ │.....k.....│+ │...........│+ │...........│+ │...........│+ │.....k.....│+ │...........│+ │...........│+ └───────────┘
+ data/scenarios/Testing/836-pathfinding/836-no-path-exists1.yaml view
@@ -0,0 +1,48 @@+version: 1+name: Builtin pathfinding, unreachable goal, enclosed robot+description: |+ There is finite area to explore, so+ this will terminate quickly.+creative: false+objectives:+ - goal:+ - Flower must not be reachable.+ condition: |+ as base {+ nextDir <- path (inL ()) (inR "flower");+ return $ case nextDir (\_. true) (\_. false);+ };+solution: |+ noop;+entities:+ - name: wayfinder+ display:+ char: 'w'+ description:+ - Enables `path` command+ properties: [known, portable]+ capabilities: [path]+robots:+ - name: base+ dir: [1,0]+ devices:+ - treads+ - logger+ - ADT calculator+ - dictionary+ - wayfinder+known: [mountain, flower, tree]+world:+ palette:+ 'B': [grass, null, base]+ '.': [grass]+ 'x': [stone, mountain]+ 'f': [grass, flower]+ 'T': [grass, tree]+ upperleft: [0, 0]+ map: |+ xxxxx......+ x...x......+ x.B.x...f..+ x...x......+ xxxxx......
+ data/scenarios/Testing/836-pathfinding/836-no-path-exists2.yaml view
@@ -0,0 +1,52 @@+version: 1+name: Builtin pathfinding, unreachable goal, enclosed goal+description: |+ There is infinite area to explore, so+ this will fail to terminate unless+ a limit is set on the max distance.++ In this scenario, we fall back onto the internal distance limit.+ Normally, it would be very expensive to allow this goal condition+ check to run upon every tick. But in this case, we should+ have won the scenario by the first tick.+creative: false+objectives:+ - goal:+ - Flower must not be reachable.+ condition: |+ as base {+ nextDir <- path (inL ()) (inR "flower");+ return $ case nextDir (\_. true) (\_. false);+ };+solution: |+ noop;+entities:+ - name: wayfinder+ display:+ char: 'w'+ description:+ - Enables `path` command+ properties: [known, portable]+ capabilities: [path]+robots:+ - name: base+ dir: [1,0]+ devices:+ - treads+ - logger+ - ADT calculator+ - dictionary+ - wayfinder+world:+ palette:+ 'B': [grass, null, base]+ '.': [grass]+ 'x': [stone, mountain]+ 'f': [grass, flower]+ upperleft: [0, 0]+ map: |+ ......xxxxx+ ......x...x+ ..B...x.f.x+ ......x...x+ ......xxxxx
+ data/scenarios/Testing/836-pathfinding/836-path-exists-distance-limit-reachable.yaml view
@@ -0,0 +1,33 @@+version: 1+name: Builtin pathfinding - entity target+description: |+ Use distance limit with `path` command+creative: false+objectives:+ - goal:+ - Flower must be reachable within 3 cells.+ condition: |+ as base {+ nextDir <- path (inR 3) (inR "flower");+ return $ case nextDir (\_. false) (\_. true);+ };+solution: |+ move;+robots:+ - name: base+ dir: [1,0]+ devices:+ - treads+ - logger+ - dictionary+ - grabber+world:+ palette:+ 'B': [grass, null, base]+ '.': [grass]+ 'f': [grass, flower]+ upperleft: [0, 0]+ map: |+ .......+ .B...f.+ .......
+ data/scenarios/Testing/836-pathfinding/836-path-exists-distance-limit-unreachable.yaml view
@@ -0,0 +1,35 @@+version: 1+name: Builtin pathfinding - entity target+description: |+ Use distance limit with `path` command+creative: false+objectives:+ - goal:+ - Flower must NOT be reachable within 3 cells.+ condition: |+ as base {+ nextDir <- path (inR 3) (inR "flower");+ return $ case nextDir (\_. true) (\_. false);+ };+solution: |+ turn back;+ move;+robots:+ - name: base+ dir: [1,0]+ devices:+ - treads+ - logger+ - dictionary+ - grabber+world:+ palette:+ 'B': [grass, null, base]+ '.': [grass]+ 'f': [grass, flower]+ upperleft: [0, 0]+ map: |+ .......+ ..B..f.+ .......+
+ data/scenarios/Testing/836-pathfinding/836-path-exists-find-entity-unwalkable.yaml view
@@ -0,0 +1,51 @@+version: 1+name: Builtin pathfinding - entity target+description: |+ Use `path` command to navigate to an entity+creative: false+objectives:+ - goal:+ - Reach and face the water.+ condition: |+ as base {+ itemAhead <- scan forward;+ return $ case itemAhead (\_. false) (\item. item == "water");+ };+solution: |+ run "scenarios/Testing/836-pathfinding/_836-path-exists/find-entity-unwalkable-solution.sw";+entities:+ - name: wayfinder+ display:+ char: 'w'+ description:+ - Enables `path` command+ properties: [known, portable]+ capabilities: [path]+robots:+ - name: base+ dir: [1,0]+ devices:+ - ADT calculator+ - branch predictor+ - comparator+ - compass+ - dictionary+ - logger+ - grabber+ - scanner+ - treads+ - wayfinder+known: [water, boulder]+world:+ dsl: |+ {blank, boulder}+ palette:+ 'B': [grass, erase, base]+ '.': [grass, erase]+ 'x': [stone, boulder]+ 'w': [grass, water]+ upperleft: [0, 0]+ map: |+ Bx...+ .x.x.+ ...xw
+ data/scenarios/Testing/836-pathfinding/836-path-exists-find-entity.yaml view
@@ -0,0 +1,47 @@+version: 1+name: Builtin pathfinding - entity target+description: |+ Use `path` command to navigate to an entity+creative: false+objectives:+ - goal:+ - Get the flower.+ condition: |+ as base {has "flower";}+solution: |+ run "scenarios/Testing/836-pathfinding/_836-path-exists/find-entity-solution.sw";+entities:+ - name: wayfinder+ display:+ char: 'w'+ description:+ - Enables `path` command+ properties: [known, portable]+ capabilities: [path]+robots:+ - name: base+ dir: [1,0]+ devices:+ - ADT calculator+ - branch predictor+ - comparator+ - compass+ - dictionary+ - logger+ - grabber+ - treads+ - wayfinder+known: [flower, mountain]+world:+ palette:+ 'B': [grass, null, base]+ '.': [grass]+ 'x': [stone, mountain]+ 'f': [grass, flower]+ upperleft: [0, 0]+ map: |+ xxxxfx+ x.Bx.x+ x.xx.x+ x....x+ xxxxxx
+ data/scenarios/Testing/836-pathfinding/836-path-exists-find-location.yaml view
@@ -0,0 +1,47 @@+version: 1+name: Builtin pathfinding - location target+description: |+ Use `path` command to navigate to a location+creative: false+objectives:+ - goal:+ - Get the flower.+ condition: |+ as base {has "flower";}+solution: |+ run "scenarios/Testing/836-pathfinding/_836-path-exists/find-location-solution.sw";+entities:+ - name: wayfinder+ display:+ char: 'w'+ description:+ - Enables `path` command+ properties: [known, portable]+ capabilities: [path]+robots:+ - name: base+ dir: [1,0]+ devices:+ - ADT calculator+ - branch predictor+ - comparator+ - compass+ - dictionary+ - logger+ - grabber+ - treads+ - wayfinder+known: [flower, mountain]+world:+ palette:+ 'B': [grass, null, base]+ '.': [grass]+ 'x': [stone, mountain]+ 'f': [grass, flower]+ upperleft: [0, 0]+ map: |+ xxxxfx+ x.Bx.x+ x.xx.x+ x....x+ xxxxxx
@@ -0,0 +1,18 @@+def harvestPlant =+ emptyHere <- isempty;+ if emptyHere {+ watch down;+ wait 1000;+ } {+ wait 50;+ harvest;+ return ();+ };+ end;++def go =+ harvestPlant;+ go;+ end;++go;
@@ -0,0 +1,35 @@+def goDir = \f. \d.+ if (d == down) {+ eggHere <- ishere "egg";+ if eggHere {grab; return ()} {};+ return ()+ } {+ turn d;++ // An obstruction might arise after+ // navigation direction is determined+ // but before we move.+ try {+ move;+ } {};+ f;+ }+ end;++def followRoute = \loc.+ nextDir <- path (inL ()) (inL loc);+ case nextDir return $ goDir $ followRoute loc;+ end;++def visitNextWaypoint = \nextWpIdx.+ nextWaypointQuery <- waypoint "wp" nextWpIdx;+ followRoute $ snd nextWaypointQuery;++ visitNextWaypoint $ nextWpIdx + 1;+ end;++def go =+ visitNextWaypoint 0;+ end;++go;
@@ -0,0 +1,16 @@++def visitNextWaypoint = \nextWpIdx.+ emptyHere <- isempty;+ if emptyHere {+ try {+ place "egg";+ } {};+ } {};+ watch down;+ nextWaypointQuery <- waypoint "wp" nextWpIdx;+ teleport self $ snd nextWaypointQuery;+ wait 1000;+ visitNextWaypoint $ nextWpIdx + 1;+ end;++visitNextWaypoint 0;
+ data/scenarios/Testing/836-pathfinding/_836-path-exists/find-entity-solution.sw view
@@ -0,0 +1,14 @@+def goDir = \f. \d.+ if (d == down) {+ grab; return ()+ } {+ turn d; move; f;+ }+ end;++def followRoute =+ nextDir <- path (inL ()) (inR "flower");+ case nextDir return $ goDir followRoute;+ end;++followRoute;
+ data/scenarios/Testing/836-pathfinding/_836-path-exists/find-entity-unwalkable-solution.sw view
@@ -0,0 +1,22 @@+def goDir = \goalItem. \f. \d.+ if (d == down) {+ grab; return ()+ } {+ turn d;+ itemAhead <- scan forward;+ let isGoalAhead = case itemAhead (\_. false) (\item. item == goalItem) in+ if isGoalAhead {+ return ();+ } {+ move; f;+ };+ }+ end;++def followRoute =+ let goalItem = "water" in+ nextDir <- path (inL ()) (inR goalItem);+ case nextDir return $ goDir goalItem followRoute;+ end;++followRoute;
+ data/scenarios/Testing/836-pathfinding/_836-path-exists/find-location-solution.sw view
@@ -0,0 +1,14 @@+def goDir = \f. \d.+ if (d == down) {+ grab; return ()+ } {+ turn d; move; f;+ }+ end;++def followRoute =+ nextDir <- path (inL ()) (inL (4, 0));+ case nextDir return $ goDir followRoute;+ end;++followRoute;
+ data/scenarios/Testing/Achievements/00-ORDER.txt view
@@ -0,0 +1,1 @@+RobotIntoWater.yaml
+ data/scenarios/Testing/Achievements/RobotIntoWater.yaml view
@@ -0,0 +1,48 @@+version: 1+name: RobotIntoWater achievement test+description: Drive a robot into the water+objectives:+ - id: build+ goal:+ - Build a robot+ condition: |+ try {robotNumbered 1; return True} {return False}+ - goal:+ - Drown it+ prerequisite: build+ condition: |+ try {robotNumbered 1; return False} {return True}+solution: |+ build { turn right; move; move; move }+robots:+ - name: base+ loc: [0,0]+ dir: [0,1]+ heavy: true+ display:+ char: Ω+ attr: robot+ devices:+ - 3D printer+ - dictionary+ - grabber+ - welder+ - life support system+ - logger+ - toolkit+ - solar panel+ - workbench+ - clock+ inventory:+ - [5, 3D printer]+ - [100, treads]+ - [70, grabber]+ - [100, solar panel]+ - [50, scanner]+ - [50, clock]+ - [5, toolkit]+seed: 0+world:+ offset: true+ dsl: |+ "classic"
data/scenarios/Testing/_Validation/1221-duplicate-entities.yaml view
@@ -14,6 +14,7 @@ char: 'Y' description: - Your scooter+robots: [] world: palette: 'x': [grass, null, base]
data/scenarios/Tutorials/type-errors.yaml view
@@ -8,7 +8,7 @@ Let's see what happens when you enter something that does not type check. Try typing `turn 1`{=snippet} at the REPL prompt. Clearly this is nonsense, and the expression will be highlighted in red. To see what the error is, hit **Enter**.- A box will pop up with a type (or parser) error.+ The REPL will print out a type error. - "Some other type errors for you to try:" - | `turn move`{=snippet}
data/scenarios/Tutorials/types.yaml view
@@ -12,7 +12,8 @@ its type will be displayed in gray text at the top right of the window. - For example, if you try typing `move`, you can see that it has type `cmd unit`{=type}, which means that `move` is a command which- returns a value of the `unit`{=type} type (also written `()`).+ returns a value of the `unit`{=type} type (the only value of+ type `unit`{=type} is called `()`). - As another example, you can see that `turn` has type `dir -> cmd unit`{=type}, meaning that `turn` is a function which takes a direction as input and results in a command.
+ data/scenarios/Tutorials/world101.sw view
@@ -0,0 +1,36 @@+def tB = turn back end+def tR = turn right end+def tL = turn left end++def m = move end+def m2 = m;m end+def m4 = m2;m2 end+def m8 = m4;m4 end+def m9 = m8;m end+def m10 = m8;m2 end++def mg = m; grab end++def get_3_trees : cmd unit =+ tB; m; mg; mg; mg; tB; m4+end++def make_harvester : cmd unit =+ make "log"; make "log"; make "log";+ make "board"; make "board"; make "board";+ make "box";+ make "wooden gear"; make "wooden gear";+ make "harvester"+end++def get_lambda : cmd unit =+ m10; tR; m9; harvest; tB; m9; tL; m10+end++def solution : cmd unit =+ build {get_3_trees}; wait 16; salvage;+ make_harvester;+ build {get_lambda}; wait 50; salvage+end;++solution
data/scenarios/Tutorials/world101.yaml view
@@ -31,16 +31,16 @@ - id: get_harvester teaser: Make a harvester goal:- - Nice work! Now, use the trees to make a harvester device.+ - Nice work! Now, use the `tree`{=entity}s to make a `harvester`{=entity} device. This will require several intermediate products; try making- various things, and take a look at your available recipes (F3)+ various things, and take a look at your available recipes (**F3**) and at the recipes listed for items in your inventory. Of course, 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`+ - Now that you have a `harvester`{=entity}, you can use `harvest` instead of `grab` whenever you pick up a growing item (check for the word "growing" at the top of the item description), to leave behind a seed that will regrow. - "**TIP:** since you only have a single harvester device for now, whenever you@@ -56,6 +56,8 @@ condition: | try { as base {has "lambda"} } {return false} prerequisite: get_harvester+solution: |+ run "scenarios/Tutorials/world101.sw" robots: - name: base display:
editors/emacs/swarm-mode.el view
@@ -55,12 +55,15 @@ "selfdestruct" "move" "backup"+ "path" "push" "stride" "turn" "grab" "harvest"+ "ignite" "place"+ "ping" "give" "equip" "unequip"
+ editors/vim/swarm.lua view
@@ -0,0 +1,8 @@+if vim.fn.executable('swarm') == 1 then+ vim.lsp.start({+ name = 'Swarm Language Server',+ cmd = { 'swarm', 'lsp' },+ })+end++
+ editors/vim/swarm.vim view
@@ -0,0 +1,24 @@+syn keyword Keyword def end let in require+syn keyword Builtins self parent base if inl inr case fst snd force undefined fail not format chars split charat tochar key+syn keyword Command noop wait selfdestruct move backup path push stride turn grab harvest ignite place ping give equip unequip make has equipped count drill use build salvage reprogram say listen log view appear create halt time scout whereami waypoint detect resonate density sniff chirp watch surveil heading blocked scan upload ishere isempty meet meetall whoami setname random run return try swap atomic instant installkeyhandler teleport as robotnamed robotnumbered knows+syn keyword Direction east north west south down forward left back right+syn keyword Type int text dir bool cmd void unit actor+++syn match Comment "//.*$"+syn region MultilineComment start="/\*" end="\*/"+syn match Brackets "[\[\]\(\)\{\}]"+syn match Colon ":"+syn match String "\".*\""+syn match Number "\<[-]\=\d\+\>"++hi def link Keyword Statement+hi def link Builtins Keyword+hi def link Command Function+hi def link Direction Function+hi def link Comment Comment+hi def link MultilineComment Comment+hi def link Brackets Keyword+hi def link Colon Keyword+hi def link String String +hi def link Number Number
editors/vscode/syntaxes/swarm.tmLanguage.json view
@@ -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|charat|tochar|key|noop|wait|selfdestruct|move|backup|push|stride|turn|grab|harvest|place|give|equip|unequip|make|has|equipped|count|drill|use|build|salvage|reprogram|say|listen|log|view|appear|create|halt|time|scout|whereami|waypoint|detect|resonate|density|sniff|chirp|watch|surveil|heading|blocked|scan|upload|ishere|isempty|meet|meetall|whoami|setname|random|run|return|try|swap|atomic|instant|installkeyhandler|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|key|noop|wait|selfdestruct|move|backup|path|push|stride|turn|grab|harvest|ignite|place|ping|give|equip|unequip|make|has|equipped|count|drill|use|build|salvage|reprogram|say|listen|log|view|appear|create|halt|time|scout|whereami|waypoint|detect|resonate|density|sniff|chirp|watch|surveil|heading|blocked|scan|upload|ishere|isempty|meet|meetall|whoami|setname|random|run|return|try|swap|atomic|instant|installkeyhandler|teleport|as|robotnamed|robotnumbered|knows)\\b" } ] },
src/Swarm/App.hs view
@@ -19,8 +19,8 @@ import Data.Text.IO qualified as T import Graphics.Vty qualified as V import Swarm.Game.Failure (SystemFailure)-import Swarm.Game.Robot (ErrorLevel (..), LogSource (ErrorTrace, Said)) import Swarm.Language.Pretty (prettyText)+import Swarm.Log (LogSource (SystemLog), Severity (..)) import Swarm.ReadableIORef (mkReadonly) import Swarm.TUI.Controller import Swarm.TUI.Model@@ -33,7 +33,8 @@ type EventHandler = BrickEvent Name AppEvent -> EventM Name AppState () --- | The definition of the app used by the @brick@ library.+-- | The configuration of the Swarm app which we pass to the @brick@+-- library. app :: EventHandler -> App AppState AppEvent Name app eventHandler = App@@ -86,8 +87,8 @@ (mkReadonly appStateRef) chan - let logP p = logEvent Said ("Web API", -2) ("started on :" <> T.pack (show p))- let logE e = logEvent (ErrorTrace Error) ("Web API", -2) (T.pack e)+ let logP p = logEvent SystemLog Info "Web API" ("started on :" <> T.pack (show p))+ let logE e = logEvent SystemLog Error "Web API" (T.pack e) let s' = s & runtimeState@@ -110,7 +111,7 @@ void $ customMain initialVty buildVty (Just chan) (app eventHandler) s' -- | A demo program to run the web service directly, without the terminal application.--- This is useful to live update the code using `ghcid -W --test "Swarm.App.demoWeb"`+-- This is useful to live update the code using @ghcid -W --test "Swarm.App.demoWeb"@. demoWeb :: IO () demoWeb = do let demoPort = 8080
src/Swarm/Constant.hs view
@@ -3,21 +3,25 @@ -- | -- SPDX-License-Identifier: BSD-3-Clause ----- Constants used throughout the UI and game+-- Constants used throughout the UI and game. module Swarm.Constant where import Data.Text (Text) -- * Website constants +-- $convention -- By convention, all URL constants include trailing slashes -- when applicable. +-- | The URL for the Swarm repository. swarmRepoUrl :: Text swarmRepoUrl = "https://github.com/swarm-game/swarm/" +-- | The URL for the Swarm wiki. wikiUrl :: Text wikiUrl = swarmRepoUrl <> "wiki/" +-- | The URL for the Swarm commands cheat sheet. wikiCheatSheet :: Text wikiCheatSheet = wikiUrl <> "Commands-Cheat-Sheet"
src/Swarm/Doc/Gen.hs view
@@ -4,27 +4,28 @@ -- | -- SPDX-License-Identifier: BSD-3-Clause+--+-- Auto-generation of various forms of documentation. module Swarm.Doc.Gen (+ -- ** Main document generation function + types generateDocs, GenerateDocs (..), EditorType (..), SheetType (..),+ loadStandaloneScenario, -- ** Formatted keyword lists keywordsCommands, keywordsDirections, operatorNames, builtinFunctionList,- editorList, -- ** Wiki pages PageAddress (..),- commandsPage,- capabilityPage,- noPageAddresses, ) where import Control.Effect.Lift+import Control.Effect.Throw (Throw, throwError) import Control.Lens (view, (^.)) import Control.Lens.Combinators (to) import Control.Monad (zipWithM, zipWithM_)@@ -33,8 +34,7 @@ import Data.List (transpose) import Data.Map.Lazy (Map, (!)) import Data.Map.Lazy qualified as Map-import Data.Maybe (fromMaybe, isJust)-import Data.Sequence (Seq)+import Data.Maybe (fromMaybe, isJust, listToMaybe) import Data.Set (Set) import Data.Set qualified as Set import Data.Text (Text, unpack)@@ -42,15 +42,16 @@ import Data.Text.IO qualified as T import Data.Tuple (swap) import Swarm.Doc.Pedagogy+import Swarm.Doc.Schema.Render+import Swarm.Doc.Util import Swarm.Game.Display (displayChar) import Swarm.Game.Entity (Entity, EntityMap (entitiesByName), entityDisplay, entityName, loadEntities) import Swarm.Game.Entity qualified as E-import Swarm.Game.Failure (SystemFailure)-import Swarm.Game.Recipe (Recipe, loadRecipes, recipeInputs, recipeOutputs, recipeRequirements, recipeTime, recipeWeight)+import Swarm.Game.Failure (SystemFailure (CustomFailure))+import Swarm.Game.Recipe (Recipe, loadRecipes, recipeCatalysts, recipeInputs, recipeOutputs, recipeTime, recipeWeight) import Swarm.Game.Robot (Robot, equippedDevices, instantiateRobot, robotInventory)-import Swarm.Game.Scenario (Scenario, loadScenario, scenarioRobots)+import Swarm.Game.Scenario (Scenario, loadStandaloneScenario, scenarioRobots) import Swarm.Game.World.Gen (extractEntities)-import Swarm.Game.World.Load (loadWorlds) import Swarm.Game.World.Typecheck (Some (..), TTerm) import Swarm.Language.Capability (Capability) import Swarm.Language.Capability qualified as Capability@@ -61,7 +62,8 @@ import Swarm.Language.Text.Markdown as Markdown (docToMark) import Swarm.Language.Typecheck (inferConst) import Swarm.Util (both, listEnums, quote)-import Swarm.Util.Effect (ignoreWarnings, simpleErrorHandle)+import Swarm.Util.Effect (simpleErrorHandle)+import Swarm.Web (swarmApiMarkdown) import Text.Dot (Dot, NodeId, (.->.)) import Text.Dot qualified as Dot @@ -73,24 +75,33 @@ -- -- ---------------------------------------------------------------------------- +-- | An enumeration of the kinds of documentation we can generate. data GenerateDocs where -- | Entity dependencies by recipes. RecipeGraph :: GenerateDocs -- | Keyword lists for editors. EditorKeywords :: Maybe EditorType -> GenerateDocs- -- | List of special key names recognized by 'key' command+ -- | List of special key names recognized by 'Swarm.Language.Syntax.Key' command SpecialKeyNames :: GenerateDocs+ -- | Cheat sheets for inclusion on the Swarm wiki. CheatSheet :: PageAddress -> Maybe SheetType -> GenerateDocs -- | List command introductions by tutorial TutorialCoverage :: GenerateDocs+ -- | Web API endpoints+ WebAPIEndpoints :: GenerateDocs deriving (Eq, Show) -data EditorType = Emacs | VSCode+-- | An enumeration of the editors supported by Swarm (currently,+-- Emacs and VS Code).+data EditorType = Emacs | VSCode | Vim deriving (Eq, Show, Enum, Bounded) -data SheetType = Entities | Commands | Capabilities | Recipes+-- | An enumeration of the kinds of cheat sheets we can produce.+data SheetType = Entities | Commands | Capabilities | Recipes | Scenario deriving (Eq, Show, Enum, Bounded) +-- | A configuration record holding the URLs of the various cheat+-- sheets, to facilitate cross-linking. data PageAddress = PageAddress { entityAddress :: Text , commandsAddress :: Text@@ -99,9 +110,7 @@ } deriving (Eq, Show) -noPageAddresses :: PageAddress-noPageAddresses = PageAddress "" "" "" ""-+-- | Generate the requested kind of documentation to stdout. generateDocs :: GenerateDocs -> IO () generateDocs = \case RecipeGraph -> generateRecipe >>= putStrLn@@ -131,12 +140,16 @@ entities <- loadEntities recipes <- loadRecipes entities sendIO $ T.putStrLn $ recipePage address recipes+ Scenario -> genScenarioSchemaDocs TutorialCoverage -> renderTutorialProgression >>= putStrLn . T.unpack+ WebAPIEndpoints -> putStrLn swarmApiMarkdown -- ---------------------------------------------------------------------------- -- GENERATE KEYWORDS: LIST OF WORDS TO BE HIGHLIGHTED -- ---------------------------------------------------------------------------- +-- | Generate a list of keywords in the format expected by one of the+-- supported editors. generateEditorKeywords :: EditorType -> IO () generateEditorKeywords = \case Emacs -> do@@ -153,6 +166,13 @@ T.putStrLn $ keywordsDirections VSCode putStrLn "\nOperators:" T.putStrLn operatorNames+ Vim -> do+ putStr "syn keyword Builtins "+ T.putStr $ builtinFunctionList Vim+ putStr "\nsyn keyword Command "+ T.putStr $ keywordsCommands Vim+ putStr "\nsyn keyword Direction "+ T.putStrLn $ keywordsDirections Vim commands :: [Const] commands = filter Syntax.isCmd Syntax.allConst@@ -170,6 +190,7 @@ editorList = \case Emacs -> T.unlines . map ((" " <>) . quote) VSCode -> T.intercalate "|"+ Vim -> T.intercalate " " constSyntax :: Const -> Text constSyntax = Syntax.syntax . Syntax.constInfo@@ -182,6 +203,7 @@ keywordsDirections :: EditorType -> Text keywordsDirections e = editorList e $ map Syntax.directionSyntax Syntax.allDirs +-- | A list of the names of all the operators in the language. operatorNames :: Text operatorNames = T.intercalate "|" $ map (escape . constSyntax) operators where@@ -204,12 +226,6 @@ -- GENERATE TABLES: COMMANDS, ENTITIES AND CAPABILITIES TO MARKDOWN TABLE -- ---------------------------------------------------------------------------- -wrap :: Char -> Text -> Text-wrap c = T.cons c . flip T.snoc c--codeQuote :: Text -> Text-codeQuote = wrap '`'- escapeTable :: Text -> Text escapeTable = T.concatMap (\c -> if c == '|' then T.snoc "\\" c else T.singleton c) @@ -224,12 +240,6 @@ maxWidths :: [[Text]] -> [Int] maxWidths = map (maximum . map T.length) . transpose -addLink :: Text -> Text -> Text-addLink l t = T.concat ["[", t, "](", l, ")"]--tshow :: (Show a) => a -> Text-tshow = T.pack . show- -- --------- -- COMMANDS -- ---------@@ -391,7 +401,7 @@ escapeTable [ T.intercalate ", " (map formatCE $ view recipeInputs r) , T.intercalate ", " (map formatCE $ view recipeOutputs r)- , T.intercalate ", " (map formatCE $ view recipeRequirements r)+ , T.intercalate ", " (map formatCE $ view recipeCatalysts r) , tshow $ view recipeTime r , tshow $ view recipeWeight r ]@@ -412,20 +422,23 @@ recipePage :: PageAddress -> [Recipe Entity] -> Text recipePage = recipeTable +getBaseRobot :: Has (Throw SystemFailure) sig m => Scenario -> m Robot+getBaseRobot s = case listToMaybe $ view scenarioRobots s of+ Just r -> pure $ instantiateRobot 0 r+ Nothing -> throwError $ CustomFailure "Scenario contains no robots"+ -- ---------------------------------------------------------------------------- -- GENERATE GRAPHVIZ: ENTITY DEPENDENCIES BY RECIPES -- ---------------------------------------------------------------------------- generateRecipe :: IO String generateRecipe = simpleErrorHandle $ do- entities <- loadEntities- recipes <- loadRecipes entities- worlds <- ignoreWarnings @(Seq SystemFailure) $ loadWorlds entities- classic <- fst <$> loadScenario "data/scenarios/classic.yaml" entities worlds- return . Dot.showDot $ recipesToDot classic (worlds ! "classic") entities recipes+ (classic, (worlds, entities, recipes)) <- loadStandaloneScenario "data/scenarios/classic.yaml"+ baseRobot <- getBaseRobot classic+ return . Dot.showDot $ recipesToDot baseRobot (worlds ! "classic") entities recipes -recipesToDot :: Scenario -> Some (TTerm '[]) -> EntityMap -> [Recipe Entity] -> Dot ()-recipesToDot classic classicTerm emap recipes = do+recipesToDot :: Robot -> Some (TTerm '[]) -> EntityMap -> [Recipe Entity] -> Dot ()+recipesToDot baseRobot classicTerm emap recipes = do Dot.attribute ("rankdir", "LR") Dot.attribute ("ranksep", "2") world <- diamond "World"@@ -443,8 +456,8 @@ -- -------------------------------------------------------------------------- -- 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+ let devs = startingDevices baseRobot+ inv = startingInventory baseRobot worldEntities = case classicTerm of Some _ t -> extractEntities t levels = recipeLevels recipes (Set.unions [worldEntities, devs]) -- --------------------------------------------------------------------------@@ -490,19 +503,19 @@ -- order entities into clusters based on how "far" they are from -- what is available at the start - see 'recipeLevels'. bottom <- wrapBelowAbove worldEntities- ls <- zipWithM subLevel [1 ..] (tail levels)+ ls <- zipWithM subLevel [1 ..] (drop 1 levels) let invisibleLine = zipWithM_ (.~>.) tls <- mapM (const hiddenNode) levels bls <- mapM (const hiddenNode) levels invisibleLine tls bls- invisibleLine bls (tail tls)+ invisibleLine bls (drop 1 tls) let sameBelowAbove (b1, t1) (b2, t2) = Dot.same [b1, b2] >> Dot.same [t1, t2] zipWithM_ sameBelowAbove (bottom : ls) (zip bls tls) -- -------------------------------------------------------------------------- -- add node for the world and draw a line to each entity found in the wild -- finally draw recipes let recipeInOut r = [(snd i, snd o) | i <- r ^. recipeInputs, o <- r ^. recipeOutputs]- recipeReqOut r = [(snd q, snd o) | q <- r ^. recipeRequirements, o <- r ^. recipeOutputs]+ recipeReqOut r = [(snd q, snd o) | q <- r ^. recipeCatalysts, o <- r ^. recipeOutputs] recipesToPairs f rs = both nid <$> nubOrd (concatMap f rs) mapM_ (uncurry (.->.)) (recipesToPairs recipeInOut recipes) mapM_ (uncurry (---<>)) (recipesToPairs recipeReqOut recipes)@@ -526,7 +539,7 @@ recipeLevels :: [Recipe Entity] -> Set Entity -> [Set Entity] recipeLevels recipes start = levels where- recipeParts r = ((r ^. recipeInputs) <> (r ^. recipeRequirements), r ^. recipeOutputs)+ recipeParts r = ((r ^. recipeInputs) <> (r ^. recipeCatalysts), r ^. recipeOutputs) m :: [(Set Entity, Set Entity)] m = map (both (Set.fromList . map snd) . recipeParts) recipes levels :: [Set Entity]@@ -540,14 +553,11 @@ then ls else go (n : ls) (Set.union n known) -startingHelper :: Scenario -> Robot-startingHelper = instantiateRobot 0 . head . view scenarioRobots--startingDevices :: Scenario -> Set Entity-startingDevices = Set.fromList . map snd . E.elems . view equippedDevices . startingHelper+startingDevices :: Robot -> Set Entity+startingDevices = Set.fromList . map snd . E.elems . view equippedDevices -startingInventory :: Scenario -> Map Entity Int-startingInventory = Map.fromList . map swap . E.elems . view robotInventory . startingHelper+startingInventory :: Robot -> Map Entity Int+startingInventory = Map.fromList . map swap . E.elems . view robotInventory -- | Ignore utility entities that are just used for tutorials and challenges. ignoredEntities :: Set Text
src/Swarm/Doc/Pedagogy.hs view
@@ -6,8 +6,9 @@ -- Assess pedagogical soundness of the tutorials. -- -- Approach:+-- -- 1. Obtain a list of all of the tutorial scenarios, in order--- 2. Search their "solution" code for `commands`+-- 2. Search their \"solution\" code for `commands` -- 3. "fold" over the tutorial list, noting which tutorial was first to introduce each command module Swarm.Doc.Pedagogy ( renderTutorialProgression,@@ -38,7 +39,7 @@ import Swarm.Language.Module (Module (..)) import Swarm.Language.Pipeline (ProcessedTerm (..)) import Swarm.Language.Syntax-import Swarm.Language.Text.Markdown (findCode)+import Swarm.Language.Text.Markdown (docToText, findCode) import Swarm.Language.Types (Polytype) import Swarm.TUI.Controller (getTutorials) import Swarm.Util.Effect (ignoreWarnings, simpleErrorHandle)@@ -174,7 +175,7 @@ intercalate [""] [ pure . surround "`" . T.pack $ view scenarioPath si- , pure . surround "*" . T.strip $ view scenarioDescription s+ , pure . surround "*" . T.strip . docToText $ view scenarioDescription s , renderSection "Introduced in solution" . renderCmdList $ M.keysSet novelCmds , renderSection "Referenced in description" $ renderCmdList dCmds ]@@ -211,6 +212,10 @@ renderCmdList :: Set Const -> [Text] renderCmdList = renderList . map linkifyCommand . cmdSetToSortedText +-- | Generate a document which lists all the tutorial scenarios,+-- highlighting for each one which commands are introduced for the+-- first time in the canonical solution, and which commands are+-- referenced in the tutorial description. renderTutorialProgression :: IO Text renderTutorialProgression = processAndRender <$> loadScenarioCollection
+ src/Swarm/Doc/Schema/Arrangement.hs view
@@ -0,0 +1,42 @@+-- |+-- SPDX-License-Identifier: BSD-3-Clause+--+-- Graph-based heuristics for arranging the+-- order of sections in the schema docs+module Swarm.Doc.Schema.Arrangement (sortAndPruneSchemas) where++import Data.Graph+import Data.Set qualified as Set+import Swarm.Doc.Schema.Parse+import Swarm.Doc.Schema.Refined+import Swarm.Doc.Schema.SchemaType++-- | Sort the schemas in topological order.+--+-- Only includes schema files that are reachable from+-- the root schema+-- (i.e. exclude @entities.json@ and @recipes.json@,+-- which are used independently to validate @entities.yaml@+-- and @recipes.yaml@).+sortAndPruneSchemas ::+ SchemaIdReference ->+ [SchemaData] ->+ [SchemaData]+sortAndPruneSchemas rootSchemaKey schemas =+ reverse . flattenSCCs . stronglyConnComp $ reachableEdges+ where+ rawEdgeList = map getNodeEdgesEntry schemas+ (graph, _nodeFromVertex, vertexFromKey) = graphFromEdges rawEdgeList+ reachableVertices = Set.fromList $ maybe [] (reachable graph) $ vertexFromKey rootSchemaKey++ reachableEdges = filter f rawEdgeList+ f (_, k, _) = maybe False (`Set.member` reachableVertices) . vertexFromKey $ k++getNodeEdgesEntry ::+ SchemaData ->+ (SchemaData, SchemaIdReference, [SchemaIdReference])+getNodeEdgesEntry sd@(SchemaData fp schem _) =+ ( sd+ , fromFilePath fp+ , Set.toList $ extractReferences $ content schem+ )
+ src/Swarm/Doc/Schema/Parse.hs view
@@ -0,0 +1,52 @@+-- |+-- SPDX-License-Identifier: BSD-3-Clause+--+-- There are no modern, comprehensive JSON Schema parsing+-- libraries in Haskell, as explained in+-- <this post https://dev.to/sshine/a-review-of-json-schema-libraries-for-haskell-321>.+--+-- Therefore, a bespoke parser for a small subset of JSON Schema is implemented here,+-- simply for rendering Markdown documentation from Swarm's schema.+module Swarm.Doc.Schema.Parse where++import Control.Applicative ((<|>))+import Data.Aeson+import Data.Map (Map)+import Data.Maybe (fromMaybe)+import Data.Text (Text)+import Swarm.Doc.Schema.Refined+import Text.Pandoc++-- | Includes everything needed to+-- render the schema to markdown+data SchemaData = SchemaData+ { schemaPath :: FilePath+ , schemaContent :: ToplevelSchema+ , markdownFooters :: [Pandoc]+ }++data Members+ = ObjectProperties (Map Text SwarmSchema)+ | ListMembers (ItemDescription SwarmSchema)+ deriving (Eq, Ord, Show)++data ToplevelSchema = ToplevelSchema+ { title :: Text+ , description :: Maybe Pandoc+ , content :: SwarmSchema+ , members :: Maybe Members+ , footerPaths :: [FilePath]+ }+ deriving (Eq, Ord, Show)++instance FromJSON ToplevelSchema where+ parseJSON x = do+ rawSchema :: rawSchema <- parseJSON x+ swarmSchema <- toSwarmSchema rawSchema++ theTitle <- maybe (fail "Schema requires a title") return $ _title rawSchema+ let theFooters = fromMaybe [] $ _footers rawSchema+ maybeMembers =+ ObjectProperties <$> properties swarmSchema+ <|> ListMembers <$> itemsDescription swarmSchema+ return $ ToplevelSchema theTitle (objectDescription swarmSchema) swarmSchema maybeMembers theFooters
+ src/Swarm/Doc/Schema/Refined.hs view
@@ -0,0 +1,145 @@+{-# LANGUAGE OverloadedStrings #-}++-- |+-- SPDX-License-Identifier: BSD-3-Clause+--+-- Refined JSON schema after converting+-- all JSON Value types to their specific sum types+module Swarm.Doc.Schema.Refined where++import Control.Applicative ((<|>))+import Data.Aeson+import Data.List.Extra (replace)+import Data.Map (Map)+import Data.Map qualified as M+import Data.Maybe (fromMaybe, mapMaybe)+import Data.Set (Set)+import Data.Set qualified as Set+import Data.Text (Text)+import Data.Text qualified as T+import GHC.Generics (Generic)+import Swarm.Doc.Schema.SchemaType+import System.FilePath (takeBaseName)+import Text.Pandoc+import Text.Pandoc.Builder++-- * Basic++schemaJsonOptions :: Options+schemaJsonOptions =+ defaultOptions+ { fieldLabelModifier = replace "S" "$" . drop 1 -- drops leading underscore+ }++-- | A single record that encompasses all possible objects+-- in a JSON schema. All fields are optional.+data SchemaRaw = SchemaRaw+ { _description :: Maybe Text+ , _default :: Maybe Value+ , _title :: Maybe Text+ , _type :: Maybe (SingleOrList Text)+ , _name :: Maybe Text+ , _properties :: Maybe (Map Text SwarmSchema)+ , _items :: Maybe (ItemDescription SwarmSchema)+ , _examples :: Maybe [Value]+ , _Sref :: Maybe Text+ , _oneOf :: Maybe [SchemaRaw]+ , _footers :: Maybe [FilePath]+ , _additionalProperties :: Maybe Bool+ }+ deriving (Eq, Ord, Show, Generic)++instance FromJSON SchemaRaw where+ parseJSON = genericParseJSON schemaJsonOptions++extractSchemaType :: SchemaRaw -> Maybe SchemaType+extractSchemaType rawSchema =+ mkReference <$> _Sref rawSchema+ <|> getTypeFromItems+ <|> Simple <$> _type rawSchema+ <|> Alternatives . mapMaybe extractSchemaType <$> _oneOf rawSchema+ where+ mkReference = Reference . SchemaIdReference . T.pack . takeBaseName . T.unpack++ getTypeFromItems :: Maybe SchemaType+ getTypeFromItems = do+ itemsThing <- _items rawSchema+ case itemsThing of+ ItemList _ -> Nothing+ ItemType x -> Just $ ListOf $ schemaType x++-- * Refined++data ItemDescription a+ = ItemList [a]+ | ItemType a+ deriving (Eq, Ord, Show)++instance (FromJSON a) => FromJSON (ItemDescription a) where+ parseJSON x =+ ItemList <$> parseJSON x+ <|> ItemType <$> parseJSON x++getSchemaReferences :: SchemaType -> [SchemaIdReference]+getSchemaReferences = \case+ Simple _ -> []+ Alternatives xs -> concatMap getSchemaReferences xs+ Reference x -> pure x+ ListOf x -> getSchemaReferences x++-- | A subset of all JSON schemas, conforming to internal Swarm conventions.+--+-- Conveniently, this extra representation layer+-- is able to enforce (via 'toSwarmSchema') that all "object"+-- definitions in the schema contain the @"additionalProperties": true@ attribute.+data SwarmSchema = SwarmSchema+ { schemaType :: SchemaType+ , defaultValue :: Maybe Value+ , objectDescription :: Maybe Pandoc+ , properties :: Maybe (Map Text SwarmSchema)+ , itemsDescription :: Maybe (ItemDescription SwarmSchema)+ , examples :: [Value]+ }+ deriving (Eq, Ord, Show)++instance FromJSON SwarmSchema where+ parseJSON x = do+ rawSchema :: rawSchema <- parseJSON x+ toSwarmSchema rawSchema++getMarkdown :: MonadFail m => Text -> m Pandoc+getMarkdown desc = case runPure (readMarkdown def desc) of+ Right d -> return d+ Left err -> fail $ T.unpack $ renderError err++toSwarmSchema :: MonadFail m => SchemaRaw -> m SwarmSchema+toSwarmSchema rawSchema = do+ theType <- maybe (fail "Unspecified sub-schema type") return maybeType+ markdownDescription <- mapM getMarkdown $ _description rawSchema++ if null (_properties rawSchema) || not (fromMaybe True (_additionalProperties rawSchema))+ then return ()+ else fail "All objects must specify '\"additionalProperties\": true'"++ return+ SwarmSchema+ { schemaType = theType+ , defaultValue = _default rawSchema+ , objectDescription = markdownDescription <|> doc . plain . text <$> _name rawSchema+ , examples = fromMaybe [] $ _examples rawSchema+ , properties = _properties rawSchema+ , itemsDescription = _items rawSchema+ }+ where+ maybeType = extractSchemaType rawSchema++-- * Utilities++-- | Recursively extract references to other schemas+extractReferences :: SwarmSchema -> Set SchemaIdReference+extractReferences s = thisRefList <> otherRefLists+ where+ thisRefList = Set.fromList . getSchemaReferences $ schemaType s++ otherSchemas = maybe [] M.elems $ properties s+ otherRefLists = Set.unions $ map extractReferences otherSchemas
+ src/Swarm/Doc/Schema/Render.hs view
@@ -0,0 +1,194 @@+{-# LANGUAGE OverloadedStrings #-}++-- |+-- SPDX-License-Identifier: BSD-3-Clause+--+-- Render a markdown document fragment+-- from the Scenario JSON schema files.+module Swarm.Doc.Schema.Render where++import Control.Arrow (left, (&&&))+import Control.Monad.Except (runExceptT)+import Control.Monad.IO.Class (liftIO)+import Control.Monad.Trans.Except (except)+import Data.Aeson+import Data.List (intersperse)+import Data.Map (Map)+import Data.Map.Strict qualified as M+import Data.Maybe (fromMaybe)+import Data.Scientific (FPFormat (..), Scientific, formatScientific)+import Data.Text qualified as T+import Data.Text.IO qualified as TIO+import Data.Vector qualified as V+import Swarm.Doc.Schema.Arrangement+import Swarm.Doc.Schema.Parse+import Swarm.Doc.Schema.Refined+import Swarm.Doc.Schema.SchemaType+import Swarm.Doc.Util+import Swarm.Util (applyWhen, brackets, quote, showT)+import System.Directory (listDirectory)+import System.FilePath (splitExtension, (<.>), (</>))+import Text.Pandoc+import Text.Pandoc.Builder+import Text.Pandoc.Walk (query)++scenariosDir :: FilePath+scenariosDir = "data/scenarios"++docFragmentsDir :: FilePath+docFragmentsDir = scenariosDir </> "doc-fragments"++schemasDir :: FilePath+schemasDir = "data/schema"++schemaExtension :: String+schemaExtension = ".json"++propertyColumnHeadings :: [T.Text]+propertyColumnHeadings =+ [ "Key"+ , "Default?"+ , "Type"+ , "Description"+ ]++listColumnHeadings :: [T.Text]+listColumnHeadings =+ [ "Index"+ , "Type"+ , "Description"+ ]++makeTitleMap :: [SchemaData] -> Map SchemaIdReference T.Text+makeTitleMap = M.fromList . map (fromFilePath . schemaPath &&& title . schemaContent)++makePandocTable :: Map SchemaIdReference T.Text -> SchemaData -> Pandoc+makePandocTable titleMap (SchemaData _ (ToplevelSchema theTitle theDescription _schema theMembers _) parsedFooters) =+ setTitle (text "JSON Schema for Scenarios") $+ doc (header 3 (text theTitle))+ <> fromMaybe mempty theDescription+ <> maybe mempty mkTable theMembers+ <> mconcat parsedFooters+ where+ renderItems someStuff = case someStuff of+ ItemType x -> plain $ text "List of " <> listToText titleMap (schemaType x)+ ItemList xs ->+ makePropsTable False listColumnHeadings titleMap+ . M.fromList+ $ zip (map tshow [0 :: Int ..]) xs++ mkTable x = doc $ case x of+ ObjectProperties props -> makePropsTable True propertyColumnHeadings titleMap props+ ListMembers someStuff -> renderItems someStuff++genPropsRow :: Bool -> Map SchemaIdReference T.Text -> (T.Text, SwarmSchema) -> [Blocks]+genPropsRow includeDefaultColumn titleMap (k, x) =+ firstColumn : applyWhen includeDefaultColumn (defaultColumn :) tailColumns+ where+ firstColumn = plain $ code k+ defaultColumn = maybe mempty (plain . code . renderValue) $ defaultValue x+ tailColumns =+ [ plain . listToText titleMap $ schemaType x+ , fromList $ maybe [] (query id) $ objectDescription x+ ]++makePropsTable ::+ Bool ->+ [T.Text] ->+ Map SchemaIdReference T.Text ->+ Map T.Text SwarmSchema ->+ Blocks+makePropsTable includeDefaultColumn headingsList titleMap =+ simpleTable headerRow . map (genPropsRow includeDefaultColumn titleMap) . M.toList+ where+ headerRow = map (plain . text) headingsList++type FileStemAndExtension = (FilePath, String)++recombineExtension :: FileStemAndExtension -> FilePath+recombineExtension (filenameStem, fileExtension) =+ filenameStem <.> fileExtension++genMarkdown :: [SchemaData] -> Either T.Text T.Text+genMarkdown schemaThings =+ left renderError $+ runPure $+ writeMarkdown (def {writerExtensions = extensionsFromList [Ext_pipe_tables]}) pd+ where+ titleMap = makeTitleMap schemaThings+ pd =+ mconcat $+ map (makePandocTable titleMap) $+ sortAndPruneSchemas (fromFilePath "scenario") schemaThings++parseSchemaFile :: FileStemAndExtension -> IO (Either T.Text ToplevelSchema)+parseSchemaFile stemAndExtension =+ left (prependPath . T.pack) <$> eitherDecodeFileStrict fullPath+ where+ prependPath = ((T.unwords ["in", quote (T.pack filename)] <> ": ") <>)+ filename = recombineExtension stemAndExtension+ fullPath = schemasDir </> filename++loadFooterContent :: (FilePath, ToplevelSchema) -> IO SchemaData+loadFooterContent (fp, schem) = do+ xs <- mapM (TIO.readFile . (scenariosDir </>)) $ footerPaths schem+ parsedFooters <- mapM getMarkdown xs+ return $+ SchemaData+ fp+ schem+ parsedFooters++genScenarioSchemaDocs :: IO ()+genScenarioSchemaDocs = do+ dirContents <- listDirectory schemasDir+ let inputFiles = filter ((== schemaExtension) . snd) $ map splitExtension dirContents+ xs <- mapM (sequenceA . (recombineExtension &&& parseSchemaFile)) inputFiles++ result <- runExceptT $ do+ schemaTuples <- except $ traverse sequenceA xs+ things <- liftIO $ mapM loadFooterContent schemaTuples+ myMarkdown <- except $ genMarkdown things+ docHeader <- liftIO $ TIO.readFile "data/scenarios/doc-fragments/header.md"+ liftIO . writeFile (docFragmentsDir </> "SCHEMA.md") . T.unpack $ docHeader <> myMarkdown++ case result of+ Left e -> print $ unwords ["Failed:", T.unpack e]+ Right _ -> return ()++renderValue :: Value -> T.Text+renderValue = \case+ Object obj -> showT obj+ Array arr -> brackets . T.intercalate ", " . map renderValue $ V.toList arr+ String t -> quote t+ Number num -> T.pack $ formatNumberCompact num+ Bool b -> showT b+ Null -> "null"++fragmentHref :: Map SchemaIdReference T.Text -> SchemaIdReference -> T.Text+fragmentHref titleMap r@(SchemaIdReference ref) =+ T.cons '#' . T.toLower . T.replace " " "-" $ x+ where+ x = M.findWithDefault ref r titleMap++listToText :: Map SchemaIdReference T.Text -> SchemaType -> Inlines+listToText titleMap = \case+ Simple xs -> renderAlternatives $ map code $ getList xs+ Alternatives xs -> renderAlternatives $ map (listToText titleMap) xs+ Reference r@(SchemaIdReference x) -> schemaLink r x+ ListOf x -> listToText titleMap x <> text " list"+ where+ renderAlternatives = mconcat . intersperse (text " or ")+ schemaLink r = link (fragmentHref titleMap r) "Link to object properties" . text++-- |+-- Strips trailing zeros and decimal point from a floating-point number+-- when possible.+--+-- Obtained from here: https://stackoverflow.com/a/35980995/105137+formatNumberCompact :: Scientific -> String+formatNumberCompact v+ | v == 0 = "0"+ | abs v < 1e-5 || abs v > 1e10 = formatScientific Exponent Nothing v+ | v - fromIntegral (floor v :: Integer) == 0 = formatScientific Fixed (Just 0) v+ | otherwise = formatScientific Generic Nothing v
+ src/Swarm/Doc/Schema/SchemaType.hs view
@@ -0,0 +1,38 @@+-- |+-- SPDX-License-Identifier: BSD-3-Clause+--+-- Representation of the "type" of a schema.+module Swarm.Doc.Schema.SchemaType where++import Control.Applicative ((<|>))+import Data.Aeson+import Data.Text (Text)+import Data.Text qualified as T+import System.FilePath (takeBaseName)++newtype SingleOrList a = SingleOrList+ { getList :: [a]+ }+ deriving (Eq, Ord, Show)++instance (FromJSON a) => FromJSON (SingleOrList a) where+ parseJSON x =+ fmap SingleOrList $+ pure <$> parseJSON x <|> parseJSON x++data SchemaType+ = -- | A basic built-in type+ Simple (SingleOrList Text)+ | -- | Any one of multiple possible schema types+ Alternatives [SchemaType]+ | -- | A reference to a schema defined elsewhere+ Reference SchemaIdReference+ | -- | Members of a list, all of the given schema type+ ListOf SchemaType+ deriving (Eq, Ord, Show)++newtype SchemaIdReference = SchemaIdReference Text+ deriving (Eq, Ord, Show)++fromFilePath :: FilePath -> SchemaIdReference+fromFilePath = SchemaIdReference . T.pack . takeBaseName
+ src/Swarm/Doc/Util.hs view
@@ -0,0 +1,22 @@+{-# LANGUAGE OverloadedStrings #-}++-- |+-- SPDX-License-Identifier: BSD-3-Clause+--+-- Utilities for generating doc markup+module Swarm.Doc.Util where++import Data.Text (Text)+import Data.Text qualified as T++wrap :: Char -> Text -> Text+wrap c = T.cons c . flip T.snoc c++codeQuote :: Text -> Text+codeQuote = wrap '`'++addLink :: Text -> Text -> Text+addLink l t = T.concat ["[", t, "](", l, ")"]++tshow :: (Show a) => a -> Text+tshow = T.pack . show
src/Swarm/Game/Achievement/Attainment.hs view
@@ -3,8 +3,13 @@ -- | -- SPDX-License-Identifier: BSD-3-Clause ----- Metadata about achievements that the player has obtained-module Swarm.Game.Achievement.Attainment where+-- Metadata about achievements that the player has obtained.+module Swarm.Game.Achievement.Attainment (+ Attainment (..),+ achievement,+ maybeScenarioPath,+ obtainedAt,+) where import Control.Lens hiding (from, (<.>)) import Data.Aeson (@@ -19,11 +24,16 @@ import GHC.Generics (Generic) import Swarm.Game.Achievement.Definitions +-- | A record holding an achievement along with some metadata to+-- record the time at which the achievement was obtained, and the+-- scenario in which it was achieved. data Attainment = Attainment { _achievement :: CategorizedAchievement+ -- ^ The achievement. , _maybeScenarioPath :: Maybe FilePath- -- ^ from which scenario was it obtained?+ -- ^ From which scenario was it obtained? , _obtainedAt :: ZonedTime+ -- ^ What time was it obtained? } deriving (Generic) @@ -47,5 +57,5 @@ achievementJsonOptions :: Options achievementJsonOptions = defaultOptions- { fieldLabelModifier = tail -- drops leading underscore+ { fieldLabelModifier = drop 1 -- drops leading underscore }
src/Swarm/Game/Achievement/Definitions.hs view
@@ -2,13 +2,28 @@ -- SPDX-License-Identifier: BSD-3-Clause -- -- Definitions of all possible achievements.-module Swarm.Game.Achievement.Definitions where+module Swarm.Game.Achievement.Definitions (+ -- * Achievements+ CategorizedAchievement (..),+ GlobalAchievement (..),+ GameplayAchievement (..),+ listAchievements, + -- * Achievement info+ ExpectedEffort (..),+ Quotation (..),+ FlavorText (..),+ AchievementInfo (..),+) where+ import Data.Aeson import Data.Text (Text) import GHC.Generics (Generic)+import Swarm.Language.Syntax (Syntax)+import Swarm.Language.Text.Markdown (Document) import Swarm.Util +-- | How hard do we expect the achievement to be? data ExpectedEffort = Trivial | Easy@@ -16,20 +31,27 @@ | Gruelling deriving (Eq, Ord, Show, Bounded, Enum, Generic, FromJSON, ToJSON) +-- | A quotation to spice up the description of an achievement. data Quotation = Quotation { attribution :: Text , content :: Text } deriving (Eq, Show, Generic, FromJSON, ToJSON) +-- | Flavor text to spice up the description of an achievement, either+-- freeform text or a quotation. data FlavorText- = Freeform Text+ = Freeform (Document Syntax) | FTQuotation Quotation deriving (Eq, Show, Generic, FromJSON, ToJSON) +-- | Information about an achievement. See+-- "Swarm.Game.Achievement.Description" for a mapping from+-- achievements to an corresponding 'AchievementInfo' record. 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@@ -38,15 +60,17 @@ -- ^ 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+ , attainmentProcess :: Document Syntax -- ^ Precisely what must be done to obtain this achievement. , effort :: ExpectedEffort+ -- ^ How hard the achievement is expected to be. , isObfuscated :: Bool -- ^ Hides the attainment process until after the achievement is attained.- -- Best when the title + elaboration constitute a good clue.+ -- Best when the title + elaboration constitute a good clue. } deriving (Eq, Show, Generic, FromJSON, ToJSON) +-- | An achievement, categorized as either global or gameplay. data CategorizedAchievement = GlobalAchievement GlobalAchievement | GameplayAchievement GameplayAchievement@@ -64,8 +88,8 @@ instance FromJSON CategorizedAchievement where parseJSON = genericParseJSON categorizedAchievementJsonOptions --- | Achievements that entail some aggregate of actions--- across scenarios+-- | Achievements that entail some aggregate of actions across+-- scenarios, or are independent of any particular scenario. data GlobalAchievement = CompletedSingleTutorial | CompletedAllTutorials@@ -75,7 +99,7 @@ instance FromJSON GlobalAchievement instance ToJSON GlobalAchievement --- | Achievements obtained while playing a single scenario+-- | Achievements obtained while playing a single scenario. data GameplayAchievement = CraftedBitcoin | RobotIntoWater@@ -83,11 +107,13 @@ | DestroyedBase | LoseScenario | GetDisoriented+ | SwapSame deriving (Eq, Ord, Show, Bounded, Enum, Generic) instance FromJSON GameplayAchievement instance ToJSON GameplayAchievement +-- | List of all possible achievements. listAchievements :: [CategorizedAchievement] listAchievements = map GlobalAchievement listEnums
src/Swarm/Game/Achievement/Description.hs view
@@ -8,79 +8,90 @@ import Swarm.Game.Achievement.Definitions +-- | Function mapping each 'CategorizedAchievement' to an appropriate+-- 'AchievementInfo' record. This function must be updated whenever+-- a new type of achievement is added. 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-describe (GameplayAchievement GetDisoriented) =- AchievementInfo- "Playing Ostrich"- ( Just $- FTQuotation $- Quotation- "Lil Jon"- "Fire up that loud / Another round of shots / Turn down for what?"- )- "'turn down' without a compass. Congratulations, you are 'disoriented'. How are you supposed to move now?"- Easy- True+describe = \case+ GlobalAchievement CompletedSingleTutorial ->+ AchievementInfo+ "Welcome Freshmen"+ (Just $ Freeform "School is in session!")+ "Complete one of the tutorials."+ Easy+ False+ 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+ GlobalAchievement LookedAtAboutScreen ->+ AchievementInfo+ "About time!"+ Nothing+ "View the About screen."+ Trivial+ True+ GameplayAchievement CraftedBitcoin ->+ -- Bitcoin is the deepest level of the recipes+ -- hierarchy.+ AchievementInfo+ "Master of Your Craft"+ Nothing+ "Make a Bitcoin"+ Moderate+ True+ 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+ GameplayAchievement AttemptSelfDestructBase ->+ AchievementInfo+ "Call of the Void"+ (Just $ Freeform "What does that big red button do?")+ "Attempt to self-destruct your base."+ Easy+ True+ GameplayAchievement DestroyedBase ->+ AchievementInfo+ "That Could Have Gone Better"+ (Just $ Freeform "Boom.")+ "Actually destroy your base."+ Moderate+ True+ GameplayAchievement LoseScenario ->+ AchievementInfo+ "Silver Lining"+ (Just $ Freeform "Here's your consolation prize.")+ "Lose at a scenario."+ Easy+ True+ GameplayAchievement GetDisoriented ->+ AchievementInfo+ "Playing Ostrich"+ ( Just $+ FTQuotation $+ Quotation+ "Lil Jon"+ "Fire up that loud / Another round of shots / Turn down for what?"+ )+ "`turn down` without a compass. Congratulations, you are \"disoriented\". How are you supposed to move now?"+ Easy+ True+ GameplayAchievement SwapSame ->+ AchievementInfo+ "Fair Trade"+ (Just $ Freeform "The *Law of Equivalent Exchange*... taken literally.")+ "`swap` an item for itself."+ Easy+ True
src/Swarm/Game/Achievement/Persistence.hs view
@@ -4,6 +4,8 @@ -- SPDX-License-Identifier: BSD-3-Clause -- -- Load/save logic for achievements.+-- Each achievement is saved to its own file to better+-- support forward-compatibility. module Swarm.Game.Achievement.Persistence where import Control.Arrow (left)@@ -16,17 +18,18 @@ import Swarm.Game.Achievement.Definitions import Swarm.Game.Failure import Swarm.Game.ResourceLoading (getSwarmXdgDataSubdir)-import Swarm.Util.Effect (forMW, warn)+import Swarm.Util.Effect (forMW) import System.Directory (doesDirectoryExist, doesFileExist, listDirectory) import System.FilePath ((</>)) --- | Get path to swarm achievements, optionally creating necessary--- directories.+-- | Get a path to the directory where achievement records are+-- stored. If the argument is set to @True@, create the directory if+-- it does not exist. 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.+-- Returns a list of attained achievements. loadAchievementsInfo :: (Has (Accum (Seq SystemFailure)) sig m, Has (Lift IO) sig m) => m [Attainment]@@ -45,7 +48,6 @@ return $ left (AssetNotLoaded Achievement p . CanNotParseYaml) eitherDecodedFile else return . Left $ AssetNotLoaded Achievement p (EntryNot File) else do- warn $ AssetNotLoaded Achievement "." $ DoesNotExist Directory return [] -- | Save info about achievements to XDG data directory.
src/Swarm/Game/CESK.hs view
@@ -53,8 +53,6 @@ Cont, -- ** Wrappers for creating delayed change of state-- -- See 'FImmediate'. WorldUpdate (..), RobotUpdate (..), @@ -62,10 +60,10 @@ Store, Addr, emptyStore,- Cell (..),+ MemCell (..), allocate,- lookupCell,- setCell,+ lookupStore,+ setStore, -- * CESK machine states CESK (..),@@ -85,6 +83,7 @@ import Control.Lens ((^.)) import Control.Lens.Combinators (pattern Empty) import Data.Aeson (FromJSON, ToJSON)+import Data.Int (Int64) import Data.IntMap.Strict (IntMap) import Data.IntMap.Strict qualified as IM import GHC.Generics (Generic)@@ -100,13 +99,20 @@ import Swarm.Language.Syntax import Swarm.Language.Types import Swarm.Language.Value as V+import Swarm.Util.WindowedCounter (Offsettable (..)) -newtype TickNumber = TickNumber {getTickNumber :: Integer}+-- | A newtype representing a count of ticks (typically since the+-- start of a game).+newtype TickNumber = TickNumber {getTickNumber :: Int64} deriving (Eq, Ord, Show, Read, Generic, FromJSON, ToJSON) -addTicks :: Integer -> TickNumber -> TickNumber-addTicks i (TickNumber n) = TickNumber $ n + i+-- | Add an offset to a 'TickNumber'.+addTicks :: Int -> TickNumber -> TickNumber+addTicks i (TickNumber n) = TickNumber $ n + fromIntegral i +instance Offsettable TickNumber where+ offsetBy = addTicks+ instance Pretty TickNumber where pretty (TickNumber i) = pretty i @@ -184,7 +190,7 @@ -- already been evaluated; we are focusing on evaluating one -- field; and some fields have yet to be evaluated. FRcd Env [(Var, Value)] Var [(Var, Maybe Term)]- | -- | We are in the middle of evaluating a record field projection.(:*:)+ | -- | We are in the middle of evaluating a record field projection. FProj Var deriving (Eq, Show, Generic, FromJSON, ToJSON) @@ -197,11 +203,13 @@ type Addr = Int --- | 'Store' represents a store, indexing integer locations to 'Cell's.-data Store = Store {next :: Addr, mu :: IntMap Cell} deriving (Show, Eq, Generic, FromJSON, ToJSON)+-- | 'Store' represents a store, /i.e./ memory, indexing integer+-- locations to 'MemCell's.+data Store = Store {next :: Addr, mu :: IntMap MemCell}+ deriving (Show, Eq, Generic, FromJSON, ToJSON) -- | A memory cell can be in one of three states.-data Cell+data MemCell = -- | A cell starts out life as an unevaluated term together with -- its environment. E Term Env@@ -219,7 +227,7 @@ -- 'Blackhole' can be reset to 'E'. Blackhole Term Env | -- | Once evaluation is complete, we cache the final 'Value' in- -- the 'Cell', so that subsequent lookups can just use it+ -- the 'MemCell', so that subsequent lookups can just use it -- without recomputing anything. V Value deriving (Show, Eq, Generic, FromJSON, ToJSON)@@ -234,19 +242,19 @@ 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 :: Addr -> Store -> Maybe Cell-lookupCell n = IM.lookup n . mu+lookupStore :: Addr -> Store -> Maybe MemCell+lookupStore n = IM.lookup n . mu -- | Set the cell at a given index.-setCell :: Addr -> Cell -> Store -> Store-setCell n c (Store nxt m) = Store nxt (IM.insert n c m)+setStore :: Addr -> MemCell -> Store -> Store+setStore n c (Store nxt m) = Store nxt (IM.insert n c m) ------------------------------------------------------------ -- CESK machine ------------------------------------------------------------ -- | The overall state of a CESK machine, which can actually be one of--- three kinds of states. The CESK machine is named after the first+-- four kinds of states. The CESK machine is named after the first -- kind of state, and it would probably be possible to inline a -- bunch of things and get rid of the second state, but I find it -- much more natural and elegant this way. Most tutorial@@ -405,14 +413,17 @@ -- Runtime robot update -------------------------------------------------------------- --- | 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.+-- | Enumeration of robot updates. This type is used for changes by+-- /e.g./ the @drill@ command which must be carried out at a later+-- tick. Using a first-order representation (as opposed to /e.g./+-- just a @Robot -> Robot@ function) 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.+-- Note that this can not be in 'Swarm.Game.Robot' as it would create+-- a cyclic dependency. data RobotUpdate- = AddEntity Count Entity- | LearnEntity Entity+ = -- | Add copies of an entity to the robot's inventory.+ AddEntity Count Entity+ | -- | Make the robot learn about an entity.+ LearnEntity Entity deriving (Eq, Ord, Show, Generic, FromJSON, ToJSON)
src/Swarm/Game/Entity.hs view
@@ -13,16 +13,19 @@ -- are mutually recursive (an inventory contains entities, which can -- have inventories). module Swarm.Game.Entity (- -- * Properties+ -- * Entity properties+ EntityName, EntityProperty (..), GrowthTime (..), defaultGrowthTime,+ Combustibility (..),+ defaultCombustibility, -- * Entities Entity, mkEntity, - -- ** Lenses+ -- ** Fields -- $lenses entityDisplay, entityName,@@ -31,6 +34,7 @@ entityDescription, entityOrientation, entityGrowth,+ entityCombustion, entityYields, entityProperties, hasProperty,@@ -122,6 +126,10 @@ -- Properties ------------------------------------------------------------ +-- | A type representing entity names, currently a synonym for 'Text'.+-- In the future it is conceivable that it might become more complex.+type EntityName = Text+ -- | Various properties that an entity can have, which affect how -- robots can interact with it. data EntityProperty@@ -133,6 +141,9 @@ Opaque | -- | Regrows from a seed after it is harvested. Growable+ | -- | Can burn when ignited (either via 'Swarm.Language.Syntax.Ignite' or by+ -- an adjacent burning entity).+ Combustible | -- | Regenerates infinitely when grabbed or harvested. Infinite | -- | Robots drown if they walk on this without a boat.@@ -159,9 +170,34 @@ newtype GrowthTime = GrowthTime (Integer, Integer) deriving (Eq, Ord, Show, Read, Generic, Hashable, FromJSON, ToJSON) +-- | The default growth time (100, 200) for a growable entity with no+-- growth time specification. defaultGrowthTime :: GrowthTime defaultGrowthTime = GrowthTime (100, 200) +-- | Properties of combustion.+data Combustibility = Combustibility+ { ignition :: Double+ -- ^ Rate of ignition by a neighbor, per tick.+ -- If this rate is denoted \(\lambda\), the probability of+ -- ignition over a period of \(t\) ticks is \(1 - e^{-\lambda t}\).+ -- See <https://math.stackexchange.com/a/1243629>.+ , duration :: (Integer, Integer)+ -- ^ min and max tick counts for combustion to persist+ , product :: Maybe EntityName+ -- ^ what entity, if any, is left over after combustion+ }+ deriving (Eq, Ord, Show, Read, Generic, Hashable, FromJSON, ToJSON)++-- | The default combustion specification for a combustible entity+-- with no combustion specification:+--+-- * ignition rate 0.5+-- * duration (100, 200)+-- * product @ash@+defaultCombustibility :: Combustibility+defaultCombustibility = Combustibility 0.5 (100, 200) (Just "ash")+ ------------------------------------------------------------ -- Entity ------------------------------------------------------------@@ -210,7 +246,7 @@ -- ^ A hash value computed from the other fields , _entityDisplay :: Display -- ^ The way this entity should be displayed on the world map.- , _entityName :: Text+ , _entityName :: EntityName -- ^ 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@@ -224,6 +260,8 @@ -- a robot moves, it moves in the direction of its orientation. , _entityGrowth :: Maybe GrowthTime -- ^ If this entity grows, how long does it take?+ , _entityCombustion :: Maybe Combustibility+ -- ^ If this entity is combustible, how spreadable is it? , _entityYields :: Maybe Text -- ^ The name of a different entity obtained when this entity is -- grabbed.@@ -243,7 +281,7 @@ -- | The @Hashable@ instance for @Entity@ ignores the cached hash -- value and simply combines the other fields. instance Hashable Entity where- hashWithSalt s (Entity _ disp nm pl descr orient grow yld props caps inv) =+ hashWithSalt s (Entity _ disp nm pl descr orient grow combust yld props caps inv) = s `hashWithSalt` disp `hashWithSalt` nm@@ -251,6 +289,7 @@ `hashWithSalt` docToText descr `hashWithSalt` orient `hashWithSalt` grow+ `hashWithSalt` combust `hashWithSalt` yld `hashWithSalt` props `hashWithSalt` caps@@ -284,7 +323,20 @@ [Capability] -> Entity mkEntity disp nm descr props caps =- rehashEntity $ Entity 0 disp nm Nothing descr Nothing Nothing Nothing (Set.fromList props) (Set.fromList caps) empty+ rehashEntity $+ Entity+ 0+ disp+ nm+ Nothing+ descr+ Nothing+ Nothing+ Nothing+ Nothing+ (Set.fromList props)+ (Set.fromList caps)+ empty ------------------------------------------------------------ -- Entity map@@ -345,6 +397,7 @@ <*> (v .: "description") <*> v .:? "orientation" <*> v .:? "growth"+ <*> v .:? "combustion" <*> v .:? "yields" <*> v .:? "properties" .!= mempty <*> v .:? "capabilities" .!= mempty@@ -413,7 +466,7 @@ entityDisplay = hashedLens _entityDisplay (\e x -> e {_entityDisplay = x}) -- | The name of the entity.-entityName :: Lens' Entity Text+entityName :: Lens' Entity EntityName entityName = hashedLens _entityName (\e x -> e {_entityName = x}) -- | The irregular plural version of the entity's name, if there is@@ -445,6 +498,10 @@ entityGrowth :: Lens' Entity (Maybe GrowthTime) entityGrowth = hashedLens _entityGrowth (\e x -> e {_entityGrowth = x}) +-- | Susceptibility to and duration of combustion+entityCombustion :: Lens' Entity (Maybe Combustibility)+entityCombustion = hashedLens _entityCombustion (\e x -> e {_entityCombustion = x})+ -- | The name of a different entity yielded when this entity is -- grabbed, if any. entityYields :: Lens' Entity (Maybe Text)@@ -509,8 +566,8 @@ -- | Look up an entity by name in an inventory, returning a list of -- matching entities. Note, if this returns some entities, it does--- *not* mean we necessarily have any in our inventory! It just--- means we *know about* them. If you want to know whether you have+-- /not/ mean we necessarily have any in our inventory! It just+-- means we /know about/ them. If you want to know whether you have -- any, use 'lookup' and see whether the resulting 'Count' is -- positive, or just use 'countByName' in the first place. lookupByName :: Text -> Inventory -> [Entity]@@ -569,7 +626,9 @@ contains :: Inventory -> Entity -> Bool contains inv e = lookup e inv > 0 --- | Check whether an inventory has an entry for entity (used by robots).+-- | Check whether an inventory has an entry for the given entity,+-- even if there are 0 copies. In particular this is used to+-- indicate whether a robot "knows about" an entity. contains0plus :: Entity -> Inventory -> Bool contains0plus e = isJust . IM.lookup (e ^. entityHash) . counts
src/Swarm/Game/Exception.hs view
@@ -17,6 +17,7 @@ import Control.Lens ((^.)) import Data.Aeson (FromJSON, ToJSON)+import Data.List.NonEmpty (NonEmpty ((:|))) import Data.Map qualified as M import Data.Set qualified as S import Data.Text (Text)@@ -47,9 +48,10 @@ -- ------------------------------------------------------------------ --- | Suggested way to fix incapable error.+-- | Suggested way to fix things when a robot does not meet the+-- requirements to run a command. data IncapableFix- = -- | Equip the missing device on yourself/target+ = -- | 'Swarm.Language.Syntax.Equip' the missing device on yourself/target FixByEquip | -- | Add the missing device to your inventory FixByObtain@@ -66,13 +68,16 @@ InfiniteLoop | -- | A robot tried to do something for which it does not have some -- of the required capabilities. This cannot be caught by a- -- @try@ block.+ -- @try@ block. Also contains the missing requirements, the+ -- term that caused the problem, and a suggestion for how to fix+ -- things. Incapable IncapableFix Requirements Term- | -- | A command failed in some "normal" way (/e.g./ a 'Move'- -- command could not move, or a 'Grab' command found nothing to- -- grab, /etc./).+ | -- | A command failed in some "normal" way (/e.g./ a 'Swarm.Language.Syntax.Move'+ -- command could not move, or a 'Swarm.Language.Syntax.Grab' command found nothing to+ -- grab, /etc./). Can be caught by a @try@ block. CmdFailed Const Text (Maybe GameplayAchievement)- | -- | The user program explicitly called 'Undefined' or 'Fail'.+ | -- | The user program explicitly called 'Swarm.Language.Syntax.Undefined' or 'Swarm.Language.Syntax.Fail'. Can+ -- be caught by a @try@ block. User Text deriving (Eq, Show, Generic, FromJSON, ToJSON) @@ -94,6 +99,7 @@ -- INCAPABLE HELPERS -- ------------------------------------------------------------------ +-- | Pretty-print an 'IncapableFix': either "equip" or "obtain". formatIncapableFix :: IncapableFix -> Text formatIncapableFix = \case FixByEquip -> "equip"@@ -137,29 +143,29 @@ 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"- , swarmRepoUrl <> "issues/26"- ]+ unlinesExText $+ "Missing the " <> capMsg <> " for:"+ :| [ squote $ prettyText tm+ , "but no device yet provides it. See"+ , swarmRepoUrl <> "issues/26"+ ] | not (S.null caps) = unlinesExText ( "You do not have the devices required for:"- : squote (prettyText tm)+ :| squote (prettyText tm) : "Please " <> formatIncapableFix f <> ":" : (("- " <>) . formatDevices <$> filter (not . null) deviceSets) ) | otherwise = unlinesExText ( "You are missing required inventory for:"- : squote (prettyText tm)+ :| squote (prettyText tm) : "Please obtain:" : (("- " <>) . formatEntity <$> M.assocs inv) )@@ -177,5 +183,5 @@ formatEntity (e, n) = e <> " (" <> from (show n) <> ")" -- | Exceptions that span multiple lines should be indented.-unlinesExText :: [Text] -> Text-unlinesExText ts = T.unlines . (head ts :) . map (" " <>) $ tail ts+unlinesExText :: NonEmpty Text -> Text+unlinesExText (t :| ts) = T.unlines $ (t :) $ map (" " <>) ts
src/Swarm/Game/Failure.hs view
@@ -3,7 +3,8 @@ -- | -- SPDX-License-Identifier: BSD-3-Clause ----- A data type to represent system failures.+-- A data type to represent system failures (as distinct from robot+-- program failures). -- -- These failures are often not fatal and serve -- to create common infrastructure for logging.@@ -31,15 +32,19 @@ ------------------------------------------------------------ -- Failure descriptions +-- | Enumeration of various assets we can attempt to load. data AssetData = AppAsset | NameGeneration | Entities | Recipes | Worlds | Scenarios | Script deriving (Eq, Show) +-- | Overarching enumeration of various assets we can attempt to load. data Asset = Achievement | Data AssetData | History | Save deriving (Eq, Show) +-- | Enumeration type to distinguish between directories and files. data Entry = Directory | File deriving (Eq, Show) +-- | An error that occurred while attempting to load some kind of asset. data LoadingFailure = DoesNotExist Entry | EntryNot Entry@@ -58,12 +63,15 @@ -- would lead to an import cycle. Instead, we choose to just -- pretty-print typechecking errors before storing them here. +-- | A warning that arose while processing an @00-ORDER.txt@ file. data OrderFileWarning = NoOrderFile | MissingFiles (NonEmpty FilePath) | DanglingFiles (NonEmpty FilePath) deriving (Eq, Show) +-- | An enumeration of various types of failures (errors or warnings)+-- that can occur. data SystemFailure = AssetNotLoaded Asset FilePath LoadingFailure | ScenarioNotFound FilePath@@ -86,7 +94,7 @@ a -> pretty (showLowT a) instance PrettyPrec Entry where- prettyPrec = const . prettyShowLow+ prettyPrec _ = prettyShowLow instance PrettyPrec LoadingFailure where prettyPrec _ = \case
src/Swarm/Game/Location.hs view
@@ -16,6 +16,7 @@ applyTurn, relativeTo, toDirection,+ toAbsDirection, nearestDirection, fromDirection, isCardinal,@@ -24,12 +25,12 @@ east, west, - -- ** utility functions+ -- ** Utility functions manhattan, euclidean, getElemsInArea, - -- ** reexports for convenience+ -- ** Re-exports for convenience Affine (..), Point (..), origin,@@ -49,18 +50,20 @@ -- $setup -- >>> import qualified Data.Map as Map+-- >>> import Linear+-- >>> import Swarm.Language.Direction --- | A Location is a pair of (x,y) coordinates, both up to 32 bits.+-- | A t'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+-- See also the 'Swarm.Game.World.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'.+-- conversions between t'Location' and 'Swarm.Game.World.Coords'. type Location = Point V2 Int32 --- | A convenient way to pattern-match on 'Location' values.+-- | A convenient way to pattern-match on t'Location' values. pattern Location :: Int32 -> Int32 -> Location pattern Location x y = P (V2 x y) @@ -74,13 +77,13 @@ -- | A @Heading@ is a 2D vector, with 32-bit coordinates. ----- 'Location' and 'Heading' are both represented using types from+-- t'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 '.^+').+-- * The difference between two t'Location's is a 'Heading' (via '.-.').+-- * A t'Location' plus a 'Heading' is another t'Location' (via 'Linear.Affine..^+'). type Heading = V2 Int32 deriving instance ToJSON (V2 Int32)@@ -118,7 +121,12 @@ -- | The 'applyTurn' function gives the meaning of each 'Direction' by -- turning relative to the given heading or by turning to an absolute--- heading+-- heading.+--+-- >>> applyTurn (DRelative (DPlanar DLeft)) (V2 5 3)+-- V2 (-3) 5+-- >>> applyTurn (DAbsolute DWest) (V2 5 3)+-- V2 (-1) 0 applyTurn :: Direction -> Heading -> Heading applyTurn d = case d of DRelative e -> case e of@@ -131,18 +139,32 @@ -- | Mapping from heading to their corresponding cardinal directions. -- Only absolute directions are mapped.-cardinalDirs :: M.Map Heading Direction+cardinalDirs :: M.Map Heading AbsoluteDir cardinalDirs =- M.fromList $ map (toHeading &&& DAbsolute) Util.listEnums+ M.fromList $ map (toHeading &&& id) Util.listEnums -- | 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 0 (-1))+-- Just (DAbsolute DSouth)+-- >>> toDirection (V2 3 7)+-- Nothing toDirection :: Heading -> Maybe Direction-toDirection v = M.lookup v cardinalDirs+toDirection = fmap DAbsolute . toAbsDirection --- | Example:--- DWest `relativeTo` DSouth == DRight+-- | Like 'toDirection', but preserve the type guarantee of an absolute direction+toAbsDirection :: Heading -> Maybe AbsoluteDir+toAbsDirection v = M.lookup v cardinalDirs++-- | Return the 'PlanarRelativeDir' which would result in turning to+-- the first (target) direction from the second (reference) direction.+--+-- >>> DWest `relativeTo` DSouth+-- DRight+-- >>> DWest `relativeTo` DWest+-- DForward relativeTo :: AbsoluteDir -> AbsoluteDir -> PlanarRelativeDir relativeTo targetDir referenceDir = toEnum indexDiff@@ -150,20 +172,21 @@ enumCount = length (Util.listEnums :: [AbsoluteDir]) indexDiff = ((-) `on` fromEnum) targetDir referenceDir `mod` enumCount --- | Logic adapted from:--- https://gamedev.stackexchange.com/questions/49290/#comment213403_49300+-- | Compute the absolute direction nearest to a given 'Heading'.+--+-- Logic adapted from <https://gamedev.stackexchange.com/questions/49290/#comment213403_49300>. nearestDirection :: Heading -> AbsoluteDir nearestDirection coord =- orderedDirs !! index+ Util.indexWrapNonEmpty orderedDirs index where angle :: Double angle = unangle (fmap fromIntegral coord) / (2 * pi) - index = round (fromIntegral enumCount * angle) `mod` enumCount- orderedDirs = Util.listEnums- enumCount = length orderedDirs+ index :: Int+ index = round $ fromIntegral (length orderedDirs) * angle+ orderedDirs = Util.listEnumsNonempty --- | Convert a 'Direction' into a corresponding heading. 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 -> Heading@@ -179,7 +202,7 @@ euclidean :: Location -> Location -> Double euclidean p1 p2 = norm (fromIntegral <$> (p2 .-. p1)) --- | Get elements that are in manhattan distance from location.+-- | Get elements that are within a certain manhattan distance from location. -- -- >>> v2s i = [(p, manhattan origin p) | x <- [-i..i], y <- [-i..i], let p = Location x y] -- >>> v2s 0
− src/Swarm/Game/Log.hs
@@ -1,76 +0,0 @@-{-# LANGUAGE QuasiQuotes #-}-{-# LANGUAGE TemplateHaskell #-}---- |--- 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 (..),- LogLocation (..),- 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.Game.CESK (TickNumber)-import Swarm.Game.Location (Location)-import Swarm.Game.Universe (Cosmic)---- | 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)--data LogLocation a = Omnipresent | Located a- deriving (Show, Eq, Ord, Generic, FromJSON, ToJSON)---- | An entry in a robot's log.-data LogEntry = LogEntry- { _leTime :: TickNumber- -- ^ 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 :: LogLocation (Cosmic 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
@@ -6,13 +6,30 @@ -- -- A recipe represents some kind of process for transforming -- some input entities into some output entities.+--+-- Recipes support a number of different game mechanics, including:+--+-- * crafting+-- * mining+-- * randomized "loot boxes"+-- * unlocking doors+--+-- == Synchronous vs Async+-- Recipes can be completed either within the same tick+-- as execution is started, or execution may span+-- multiple ticks. It is possible for the execution+-- of multi-tick recipes to be interrupted in one way or+-- another, in which case the recipe fails without producing+-- the "outputs". module Swarm.Game.Recipe ( -- * Ingredient lists and recipes IngredientList, Recipe (..),++ -- ** Fields recipeInputs, recipeOutputs,- recipeRequirements,+ recipeCatalysts, recipeTime, recipeWeight, @@ -20,7 +37,7 @@ loadRecipes, outRecipeMap, inRecipeMap,- reqRecipeMap,+ catRecipeMap, -- * Looking up recipes MissingIngredient (..),@@ -61,14 +78,12 @@ -- game is running. type IngredientList e = [(Count, e)] --- | A recipe is just a list of input entities and a list of output--- entities (both with multiplicity). The idea is that it--- represents some kind of process where the inputs are--- transformed into the outputs.+-- | A recipe represents some kind of process where inputs are+-- transformed into outputs. data Recipe e = Recipe { _recipeInputs :: IngredientList e , _recipeOutputs :: IngredientList e- , _recipeRequirements :: IngredientList e+ , _recipeCatalysts :: IngredientList e , _recipeTime :: Integer , _recipeWeight :: Integer }@@ -89,8 +104,8 @@ recipeTime :: Lens' (Recipe e) Integer -- | Other entities which the recipe requires you to have, but which--- are not consumed by the recipe (e.g. a furnace).-recipeRequirements :: Lens' (Recipe e) (IngredientList e)+-- are not consumed by the recipe (e.g. a @\"furnace\"@).+recipeCatalysts :: Lens' (Recipe e) (IngredientList e) -- | How this recipe is weighted against other recipes. Any time -- there are multiple valid recipes that fit certain criteria, one@@ -103,12 +118,12 @@ ------------------------------------------------------------ instance ToJSON (Recipe Text) where- toJSON (Recipe ins outs reqs time weight) =+ toJSON (Recipe ins outs cats time weight) = object $ [ "in" .= ins , "out" .= outs ]- ++ ["required" .= reqs | not (null reqs)]+ ++ ["required" .= cats | not (null cats)] ++ ["time" .= time | time /= 1] ++ ["weight" .= weight | weight /= 1] @@ -185,9 +200,9 @@ inRecipeMap :: [Recipe Entity] -> IntMap [Recipe Entity] inRecipeMap = buildRecipeMap recipeInputs --- | Build a map of recipes indexed by requirements.-reqRecipeMap :: [Recipe Entity] -> IntMap [Recipe Entity]-reqRecipeMap = buildRecipeMap recipeRequirements+-- | Build a map of recipes indexed by catalysts.+catRecipeMap :: [Recipe Entity] -> IntMap [Recipe Entity]+catRecipeMap = buildRecipeMap recipeCatalysts -- | Get a list of all the recipes for the given entity. Look up an -- entity in either an 'inRecipeMap' or 'outRecipeMap' depending on@@ -196,28 +211,32 @@ recipesFor :: IntMap [Recipe Entity] -> Entity -> [Recipe Entity] recipesFor rm e = fromMaybe [] $ IM.lookup (e ^. entityHash) rm +-- | Record information about something missing from a recipe. data MissingIngredient = MissingIngredient MissingType Count Entity deriving (Show, Eq) +-- | What kind of thing is missing? data MissingType = MissingInput | MissingCatalyst deriving (Show, Eq) -- | 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 equipped.+-- inventory to be able to carry out the recipe. Catalysts are not+-- consumed and so can be used even when equipped. missingIngredientsFor :: (Inventory, Inventory) -> Recipe Entity -> [MissingIngredient]-missingIngredientsFor (inv, ins) (Recipe inps _ reqs _ _) =+missingIngredientsFor (inv, ins) (Recipe inps _ cats _ _) = mkMissing MissingInput (findLacking inv inps)- <> mkMissing MissingCatalyst (findLacking ins (findLacking inv reqs))+ <> mkMissing MissingCatalyst (findLacking ins (findLacking inv cats)) where mkMissing k = map (uncurry (MissingIngredient k)) findLacking inven = filter ((> 0) . fst) . map (countNeeded inven) countNeeded inven (need, entity) = (need - E.lookup entity inven, entity) --- | Figure out if a recipe is available, but it can be lacking items.+-- | Figure out if a recipe is available, /i.e./ if we at least know+-- about all the ingredients. Note it does not matter whether we have+-- enough of the ingredients. knowsIngredientsFor :: (Inventory, Inventory) -> Recipe Entity -> Bool knowsIngredientsFor (inv, ins) recipe =- knowsAll inv (recipe ^. recipeInputs) && knowsAll ins (recipe ^. recipeRequirements)+ knowsAll inv (recipe ^. recipeInputs) && knowsAll ins (recipe ^. recipeCatalysts) where knowsAll xs = all (E.contains xs . snd) @@ -227,12 +246,10 @@ -- or an inventory without inputs and function adding outputs if -- it was successful. make ::- -- robots inventory and equipped devices+ -- | The robot's inventory and equipped devices (Inventory, Inventory) ->- -- considered recipe+ -- | The recipe we are trying to make Recipe Entity ->- -- failure (with count of missing) or success with a new inventory,- -- a function to add results and the recipe repeated Either [MissingIngredient] (Inventory, IngredientList Entity, Recipe Entity)
src/Swarm/Game/ResourceLoading.hs view
@@ -12,6 +12,7 @@ import Control.Exception (catch) import Control.Exception.Base (IOException) import Control.Monad (forM, when, (<=<))+import Data.Array (Array, listArray) import Data.Map (Map) import Data.Map qualified as M import Data.Maybe (mapMaybe)@@ -31,10 +32,16 @@ import System.FilePath import Witch +-- | Read-only lists of adjectives and words for use in building random robot names+data NameGenerator = NameGenerator+ { adjList :: Array Int Text+ , nameList :: Array Int Text+ }+ -- | Get subdirectory from swarm data directory. -- -- This will first look in Cabal generated path and then--- try a `data` directory in 'XdgData' path.+-- 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@@ -75,7 +82,7 @@ then return fp else throwError $ AssetNotLoaded (Data asset) fp $ DoesNotExist File --- | Get a nice message suggesting to download `data` directory to 'XdgData'.+-- | Get a nice message suggesting to download @data@ directory to 'XdgData'. dataNotFound :: FilePath -> IO LoadingFailure dataNotFound f = do d <- getSwarmXdgDataSubdir False ""@@ -89,7 +96,7 @@ -- | Get path to swarm data, optionally creating necessary -- directories. This could fail if user has bad permissions--- on his own $HOME or $XDG_DATA_HOME which is unlikely.+-- 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"@@ -112,7 +119,7 @@ getSwarmHistoryPath :: Bool -> IO FilePath getSwarmHistoryPath createDirs = getSwarmXdgDataFile createDirs "history" --- | Read all the .txt files in the data/ directory.+-- | Read all the @.txt@ files in the @data/@ directory. readAppData :: (Has (Throw SystemFailure) sig m, Has (Lift IO) sig m) => m (Map Text Text)@@ -126,3 +133,20 @@ filesList <- sendIO $ forM fs (\f -> (into @Text (dropExtension f),) <$> readFileMayT (d </> f)) return $ M.fromList . mapMaybe sequenceA $ filesList++initNameGenerator :: Has (Throw SystemFailure) sig m => Map Text Text -> m NameGenerator+initNameGenerator appDataMap = do+ adjs <- getDataLines "adjectives"+ names <- getDataLines "names"+ return $+ NameGenerator+ { adjList = makeArr adjs+ , nameList = makeArr names+ }+ where+ makeArr xs = listArray (0, length xs - 1) xs+ getDataLines f = case M.lookup f appDataMap of+ Nothing ->+ throwError $+ AssetNotLoaded (Data NameGeneration) (into @FilePath f <.> "txt") (DoesNotExist File)+ Just content -> return . drop 1 . T.lines $ content
src/Swarm/Game/Robot.hs view
@@ -13,9 +13,6 @@ module Swarm.Game.Robot ( -- * Robots data - -- * Robot log entries- module Swarm.Game.Log,- -- * Robots RobotPhase (..), RID,@@ -38,6 +35,7 @@ robotEntity, robotName, trobotName,+ unwalkableEntities, robotCreatedAt, robotDisplay, robotLocation,@@ -58,8 +56,13 @@ machine, systemRobot, selfDestruct,- tickSteps, runningAtomic,+ activityCounts,+ tickStepBudget,+ tangibleCommandCount,+ commandsHistogram,+ lifetimeStepCount,+ activityWindow, -- ** Creation & instantiation mkRobot,@@ -76,11 +79,12 @@ hearingDistance, ) where -import Control.Lens hiding (contains)-import Data.Aeson (FromJSON, ToJSON)+import Control.Lens hiding (Const, contains)+import Data.Aeson qualified as Ae (FromJSON, Key, KeyValue, ToJSON (..), object, (.=)) import Data.Hashable (hashWithSalt) import Data.Kind qualified-import Data.Maybe (fromMaybe, isNothing)+import Data.Map (Map)+import Data.Maybe (catMaybes, fromMaybe, isNothing) import Data.Sequence (Seq) import Data.Sequence qualified as Seq import Data.Set (Set)@@ -94,17 +98,19 @@ import Swarm.Game.Display (Display, curOrientation, defaultRobotDisplay, invisible) import Swarm.Game.Entity hiding (empty) import Swarm.Game.Location (Heading, Location, toDirection)-import Swarm.Game.Log import Swarm.Game.Universe import Swarm.Language.Capability (Capability) import Swarm.Language.Context qualified as Ctx+import Swarm.Language.Pipeline.QQ (tmQ) import Swarm.Language.Requirement (ReqCtx)-import Swarm.Language.Syntax (Syntax)+import Swarm.Language.Syntax (Const, Syntax) import Swarm.Language.Text.Markdown (Document) import Swarm.Language.Typed (Typed (..)) import Swarm.Language.Types (TCtx) import Swarm.Language.Value as V-import Swarm.Util.Lens (makeLensesExcluding)+import Swarm.Log+import Swarm.Util.Lens (makeLensesExcluding, makeLensesNoSigs)+import Swarm.Util.WindowedCounter import Swarm.Util.Yaml import System.Clock (TimeSpec) @@ -124,7 +130,7 @@ -- ^ A store containing memory cells allocated to hold -- definitions. }- deriving (Eq, Show, Generic, FromJSON, ToJSON)+ deriving (Eq, Show, Generic, Ae.FromJSON, Ae.ToJSON) makeLenses ''RobotContext @@ -167,6 +173,85 @@ | -- | The robot record represents a concrete robot in the world. ConcreteRobot +data ActivityCounts = ActivityCounts+ { _tickStepBudget :: Int+ , _tangibleCommandCount :: Int+ , _commandsHistogram :: Map Const Int+ , _lifetimeStepCount :: Int+ , _activityWindow :: WindowedCounter TickNumber+ }+ deriving (Eq, Show, Generic, Ae.FromJSON, Ae.ToJSON)++emptyActivityCount :: ActivityCounts+emptyActivityCount =+ ActivityCounts+ { _tickStepBudget = 0+ , _tangibleCommandCount = 0+ , _commandsHistogram = mempty+ , _lifetimeStepCount = 0+ , -- NOTE: This value was chosen experimentally.+ -- TODO(#1341): Make this dynamic based on game speed.+ _activityWindow = mkWindow 64+ }++makeLensesNoSigs ''ActivityCounts++-- | A counter that is decremented upon each step of the robot within the+-- CESK machine. Initially set to 'Swarm.Game.State.robotStepsPerTick'+-- at each new tick.+--+-- The need for 'tickStepBudget' is a bit technical, and I hope I can+-- eventually find a different, better way to accomplish it.+-- Ideally, we would want each robot to execute a single+-- /command/ at every game tick, so that /e.g./ two robots+-- executing @move;move;move@ and @repeat 3 move@ (given a+-- suitable definition of @repeat@) will move in lockstep.+-- However, the second robot actually has to do more computation+-- than the first (it has to look up the definition of @repeat@,+-- reduce its application to the number 3, etc.), so its CESK+-- machine will take more steps. It won't do to simply let each+-- robot run until executing a command---because robot programs+-- can involve arbitrary recursion, it is very easy to write a+-- program that evaluates forever without ever executing a+-- command, which in this scenario would completely freeze the+-- UI. (It also wouldn't help to ensure all programs are+-- terminating---it would still be possible to effectively do+-- the same thing by making a program that takes a very, very+-- long time to terminate.) So instead, we allocate each robot+-- a certain maximum number of computation steps per tick+-- (defined in 'Swarm.Game.Step.evalStepsPerTick'), and it+-- suspends computation when it either executes a command or+-- reaches the maximum number of steps, whichever comes first.+--+-- It seems like this really isn't something the robot should be+-- keeping track of itself, but that seemed the most technically+-- convenient way to do it at the time. The robot needs some+-- way to signal when it has executed a command, which it+-- currently does by setting tickStepBudget to zero. However, that+-- has the disadvantage that when tickStepBudget becomes zero, we+-- can't tell whether that happened because the robot ran out of+-- steps, or because it executed a command and set it to zero+-- manually.+--+-- Perhaps instead, each robot should keep a counter saying how+-- many commands it has executed. The loop stepping the robot+-- can tell when the counter increments.+tickStepBudget :: Lens' ActivityCounts Int++-- | Total number of tangible commands executed over robot's lifetime+tangibleCommandCount :: Lens' ActivityCounts Int++-- | Histogram of commands executed over robot's lifetime+commandsHistogram :: Lens' ActivityCounts (Map Const Int)++-- | Total number of CESK steps executed over robot's lifetime.+-- This could be thought of as "CPU cycles" consumed, and is labeled+-- as "cycles" in the F2 dialog in the UI.+lifetimeStepCount :: Lens' ActivityCounts Int++-- | Sliding window over a span of ticks indicating ratio of activity+activityWindow :: Lens' ActivityCounts (WindowedCounter TickNumber)+ -- | 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) :: Data.Kind.Type where@@ -197,8 +282,9 @@ , _machine :: CESK , _systemRobot :: Bool , _selfDestruct :: Bool- , _tickSteps :: Int+ , _activityCounts :: ActivityCounts , _runningAtomic :: Bool+ , _unwalkableEntities :: Set EntityName , _robotCreatedAt :: TimeSpec } deriving (Generic)@@ -206,8 +292,6 @@ deriving instance (Show (RobotLocation phase), Show (RobotID phase)) => Show (RobotR phase) deriving instance (Eq (RobotLocation phase), Eq (RobotID phase)) => Eq (RobotR phase) -deriving instance (ToJSON (RobotLocation phase), ToJSON (RobotID phase)) => ToJSON (RobotR phase)- -- See https://byorgey.wordpress.com/2021/09/17/automatically-updated-cached-views-with-lens/ -- for the approach used here with lenses. @@ -221,7 +305,25 @@ type Robot = RobotR 'ConcreteRobot instance ToSample Robot where- toSamples _ = SD.noSamples+ toSamples _ = SD.singleSample sampleBase+ where+ sampleBase :: Robot+ sampleBase =+ mkRobot+ 0+ Nothing+ "base"+ "The starting robot."+ defaultCosmicLocation+ zero+ defaultRobotDisplay+ (initMachine [tmQ| move |] mempty emptyStore)+ []+ []+ False+ False+ mempty+ 0 -- In theory we could make all these lenses over (RobotR phase), but -- that leads to lots of type ambiguity problems later. In practice@@ -239,11 +341,15 @@ -- . 'entityName'@. robotEntity :: Lens' (RobotR phase) Entity +-- | Entities that the robot cannot move onto+unwalkableEntities :: Lens' Robot (Set EntityName)+ -- | The creation date of the robot. robotCreatedAt :: Lens' Robot TimeSpec --- robotName and trobotName could be generalized to robotName' ::--- Lens' (RobotR phase) Text. However, type inference does not work+-- robotName and trobotName could be generalized to+-- @robotName' :: Lens' (RobotR phase) Text@.+-- However, type inference does not work -- very well with the polymorphic version, so we export both -- monomorphic versions instead. @@ -269,22 +375,22 @@ & curOrientation .~ ((r ^. robotOrientation) >>= toDirection) setDisplay r d = r & robotEntity . entityDisplay .~ d --- | The robot's current location, represented as (x,y). This is only+-- | The robot's current location, represented as @(x,y)@. This is only -- 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.+-- to update the 'Swarm.Game.State.robotsByLocation' map as well. You can use the+-- 'Swarm.Game.Step.updateRobotLocation' function for this purpose. robotLocation :: Getter Robot (Cosmic 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'+-- called directly except by the 'Swarm.Game.Step.updateRobotLocation' function.+-- The reason is that we need to make sure the 'Swarm.Game.State.robotsByLocation' -- map stays in sync. unsafeSetRobotLocation :: Cosmic 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.+-- 'Swarm.Game.State.robotsByLocation' map to keep up-to-date. trobotLocation :: Lens' TRobot (Maybe (Cosmic Location)) trobotLocation = lens _robotLocation (\r l -> r {_robotLocation = l}) @@ -344,8 +450,8 @@ } -- | The robot's own private message log, most recent message last.--- Messages can be added both by explicit use of the 'Log' command,--- and by uncaught exceptions. Stored as a "Data.Sequence" so that+-- Messages can be added both by explicit use of the 'Swarm.Language.Syntax.Log' command,+-- and by uncaught exceptions. Stored as a 'Seq' so that -- we can efficiently add to the end and also process from beginning -- to end. Note that updating via this lens will also set the -- 'robotLogUpdated'.@@ -396,43 +502,8 @@ -- | Does this robot wish to self destruct? selfDestruct :: Lens' Robot Bool --- | The need for 'tickSteps' is a bit technical, and I hope I can--- eventually find a different, better way to accomplish it.--- Ideally, we would want each robot to execute a single--- /command/ at every game tick, so that /e.g./ two robots--- executing @move;move;move@ and @repeat 3 move@ (given a--- suitable definition of @repeat@) will move in lockstep.--- However, the second robot actually has to do more computation--- than the first (it has to look up the definition of @repeat@,--- reduce its application to the number 3, etc.), so its CESK--- machine will take more steps. It won't do to simply let each--- robot run until executing a command---because robot programs--- can involve arbitrary recursion, it is very easy to write a--- program that evaluates forever without ever executing a--- command, which in this scenario would completely freeze the--- UI. (It also wouldn't help to ensure all programs are--- terminating---it would still be possible to effectively do--- the same thing by making a program that takes a very, very--- long time to terminate.) So instead, we allocate each robot--- a certain maximum number of computation steps per tick--- (defined in 'Swarm.Game.Step.evalStepsPerTick'), and it--- suspends computation when it either executes a command or--- reaches the maximum number of steps, whichever comes first.------ It seems like this really isn't something the robot should be--- keeping track of itself, but that seemed the most technically--- convenient way to do it at the time. The robot needs some--- way to signal when it has executed a command, which it--- currently does by setting tickSteps to zero. However, that--- has the disadvantage that when tickSteps becomes zero, we--- can't tell whether that happened because the robot ran out of--- steps, or because it executed a command and set it to zero--- manually.------ Perhaps instead, each robot should keep a counter saying how--- many commands it has executed. The loop stepping the robot--- can tell when the counter increments.-tickSteps :: Lens' Robot Int+-- | Diagnostic and operational tracking of CESK steps or other activity+activityCounts :: Lens' Robot ActivityCounts -- | Is the robot currently running an atomic block? runningAtomic :: Lens' Robot Bool@@ -463,10 +534,12 @@ Bool -> -- | Is this robot heavy? Bool ->+ -- | Unwalkable entities+ Set EntityName -> -- | Creation date TimeSpec -> RobotR phase-mkRobot rid pid name descr loc dir disp m devs inv sys heavy ts =+mkRobot rid pid name descr loc dir disp m devs inv sys heavy unwalkables ts = RobotR { _robotEntity = mkEntity disp name descr [] []@@ -485,8 +558,9 @@ , _machine = m , _systemRobot = sys , _selfDestruct = False- , _tickSteps = 0+ , _activityCounts = emptyActivityCount , _runningAtomic = False+ , _unwalkableEntities = unwalkables } where inst = fromList devs@@ -512,10 +586,46 @@ <*> v ..:? "inventory" ..!= [] <*> pure sys <*> liftE (v .:? "heavy" .!= False)+ <*> liftE (v .:? "unwalkable" ..!= mempty) <*> pure 0 where mkMachine Nothing = Out VUnit emptyStore [] mkMachine (Just pt) = initMachine pt mempty emptyStore++(.=?) :: (Ae.KeyValue a, Ae.ToJSON v, Eq v) => Ae.Key -> v -> v -> Maybe a+(.=?) n v defaultVal = if defaultVal /= v then Just $ n Ae..= v else Nothing++(.==) :: (Ae.KeyValue a, Ae.ToJSON v) => Ae.Key -> v -> Maybe a+(.==) n v = Just $ n Ae..= v++instance Ae.ToJSON Robot where+ toJSON r =+ Ae.object $+ catMaybes+ [ "id" .== (r ^. robotID)+ , "name" .== (r ^. robotEntity . entityDisplay)+ , "description" .=? (r ^. robotEntity . entityDescription) $ mempty+ , "loc" .== (r ^. robotLocation)+ , "dir" .=? (r ^. robotEntity . entityOrientation) $ zero+ , "display" .=? (r ^. robotDisplay) $ (defaultRobotDisplay & invisible .~ sys)+ , "program" .== (r ^. machine)+ , "devices" .=? (map (^. _2 . entityName) . elems $ r ^. equippedDevices) $ []+ , "inventory" .=? (map (_2 %~ view entityName) . elems $ r ^. robotInventory) $ []+ , "system" .=? sys $ False+ , "heavy" .=? (r ^. robotHeavy) $ False+ , "log" .=? (r ^. robotLog) $ mempty+ , -- debug+ "capabilities" .=? (r ^. robotCapabilities) $ mempty+ , "logUpdated" .=? (r ^. robotLogUpdated) $ False+ , "context" .=? (r ^. robotContext) $ emptyRobotContext+ , "parent" .=? (r ^. robotParentID) $ Nothing+ , "createdAt" .=? (r ^. robotCreatedAt) $ 0+ , "selfDestruct" .=? (r ^. selfDestruct) $ False+ , "activity" .=? (r ^. activityCounts) $ emptyActivityCount+ , "runningAtomic" .=? (r ^. runningAtomic) $ False+ ]+ where+ sys = r ^. systemRobot -- | Is the robot actively in the middle of a computation? isActive :: Robot -> Bool
src/Swarm/Game/Scenario.hs view
@@ -44,6 +44,7 @@ loadScenario, loadScenarioFile, getScenarioPath,+ loadStandaloneScenario, ) where import Control.Arrow ((&&&))@@ -57,6 +58,7 @@ import Data.List.NonEmpty qualified as NE import Data.Map qualified as M import Data.Maybe (catMaybes, isNothing, listToMaybe)+import Data.Sequence (Seq) import Data.Text (Text) import Data.Text qualified as T import Swarm.Game.Entity@@ -74,11 +76,14 @@ import Swarm.Game.Scenario.Topography.Structure qualified as Structure import Swarm.Game.Scenario.Topography.WorldDescription import Swarm.Game.Universe+import Swarm.Game.World.Load (loadWorlds) import Swarm.Game.World.Typecheck (WorldMap) import Swarm.Language.Pipeline (ProcessedTerm) import Swarm.Language.Pretty (prettyText)+import Swarm.Language.Syntax (Syntax)+import Swarm.Language.Text.Markdown (Document) import Swarm.Util (binTuples, failT)-import Swarm.Util.Effect (throwToMaybe, withThrow)+import Swarm.Util.Effect (ignoreWarnings, throwToMaybe, withThrow) import Swarm.Util.Lens (makeLensesNoSigs) import Swarm.Util.Yaml import System.Directory (doesFileExist)@@ -94,7 +99,7 @@ { _scenarioVersion :: Int , _scenarioName :: Text , _scenarioAuthor :: Maybe Text- , _scenarioDescription :: Text+ , _scenarioDescription :: Document Syntax , _scenarioCreative :: Bool , _scenarioSeed :: Maybe Int , _scenarioAttrs :: [CustomAttr]@@ -203,7 +208,7 @@ -- | A high-level description of the scenario, shown /e.g./ in the -- menu.-scenarioDescription :: Lens' Scenario Text+scenarioDescription :: Lens' Scenario (Document Syntax) -- | Whether the scenario should start in creative mode. scenarioCreative :: Lens' Scenario Bool@@ -287,3 +292,14 @@ decodeFileEitherE (em, worldMap) fileName where adaptError = AssetNotLoaded (Data Scenarios) fileName . CanNotParseYaml++loadStandaloneScenario ::+ (Has (Throw SystemFailure) sig m, Has (Lift IO) sig m) =>+ FilePath ->+ m (Scenario, (WorldMap, EntityMap, [Recipe Entity]))+loadStandaloneScenario fp = do+ entities <- loadEntities+ recipes <- loadRecipes entities+ worlds <- ignoreWarnings @(Seq SystemFailure) $ loadWorlds entities+ scene <- fst <$> loadScenario fp entities worlds+ return (scene, (worlds, entities, recipes))
src/Swarm/Game/Scenario/Objective.hs view
@@ -35,17 +35,17 @@ -- explain the broader intention behind potentially multiple -- prerequisites. --- -- Set this to option True to display this goal in the "upcoming" section even+ -- Set this option to '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.+ -- 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.+ -- might not still hold. The condition is never re-evaluated once true. } deriving (Eq, Show, Generic, ToJSON) @@ -115,7 +115,7 @@ -- This attribute often goes along with an Achievement. objectiveHidden :: Lens' Objective Bool --- | An optional Achievement that is to be registered globally+-- | An optional achievement that is to be registered globally -- when this objective is completed. objectiveAchievement :: Lens' Objective (Maybe AchievementInfo)
src/Swarm/Game/Scenario/Objective/Validation.hs view
@@ -3,7 +3,7 @@ -- | -- SPDX-License-Identifier: BSD-3-Clause ----- Validity checking for Objective prerequisites+-- Validity checking for 'Objective' prerequisites module Swarm.Game.Scenario.Objective.Validation where import Control.Monad (unless)@@ -19,9 +19,10 @@ -- | Performs monadic validation before returning -- the "pure" construction of a wrapper record. -- This validation entails:--- 1) Ensuring that all goal references utilized in prerequisites+--+-- 1. Ensuring that all goal references utilized in prerequisites -- actually exist--- 2) Ensuring that the graph of dependencies is acyclic.+-- 2. Ensuring that the graph of dependencies is acyclic. validateObjectives :: MonadFail m => [Objective] ->
src/Swarm/Game/Scenario/Scoring/Best.hs view
@@ -2,7 +2,10 @@ {-# LANGUAGE TemplateHaskell #-} {-# OPTIONS_GHC -Wno-orphans #-} --- | Types and records for updating and retrieving+-- |+-- SPDX-License-Identifier: BSD-3-Clause+--+-- Types and records for updating and retrieving -- the best scores for a scenario. module Swarm.Game.Scenario.Scoring.Best where
src/Swarm/Game/Scenario/Scoring/CodeSize.hs view
@@ -1,5 +1,8 @@--- | Types and utilities to compute code size--- in terms of textual length and AST.+-- |+-- SPDX-License-Identifier: BSD-3-Clause+--+-- Types and utilities to compute code size+-- in terms of textual length and AST nodes. module Swarm.Game.Scenario.Scoring.CodeSize where import Control.Monad (guard)
src/Swarm/Game/Scenario/Scoring/ConcreteMetrics.hs view
@@ -1,6 +1,9 @@ {-# LANGUAGE TemplateHaskell #-} --- | Data types and instances for specific scoring methods+-- |+-- SPDX-License-Identifier: BSD-3-Clause+--+-- Data types and instances for specific scoring methods module Swarm.Game.Scenario.Scoring.ConcreteMetrics where import Control.Lens hiding (from, (<.>))
src/Swarm/Game/Scenario/Scoring/GenericMetrics.hs view
@@ -1,4 +1,7 @@--- | Data types and functions applicable across different+-- |+-- SPDX-License-Identifier: BSD-3-Clause+--+-- Data types and functions applicable across different -- scoring methods. module Swarm.Game.Scenario.Scoring.GenericMetrics where @@ -36,10 +39,10 @@ -- for incomplete games (rationale: more play = more fun), -- whereas "smaller inputs are better" for completed games. ----- Since "Maybe" has its own "Ord" instance where+-- Since 'Maybe' has its own 'Ord' instance where -- @Nothing < Just x@ regardless of @x@, when we want to--- choose the minimum value we `fmap Down` to ensure that--- the `Just` is selected while inverting the ordering of+-- choose the minimum value we @fmap Down@ to ensure that+-- the 'Just' is selected while inverting the ordering of -- the inner member. chooseBetter :: Ord a =>
src/Swarm/Game/Scenario/Status.hs view
@@ -2,7 +2,10 @@ {-# LANGUAGE TemplateHaskell #-} {-# OPTIONS_GHC -Wno-orphans #-} --- | High-level status of scenario play.+-- |+-- SPDX-License-Identifier: BSD-3-Clause+--+-- High-level status of scenario play. -- Representation of progress, logic for updating. module Swarm.Game.Scenario.Status where @@ -26,8 +29,9 @@ import Swarm.Util.Lens (makeLensesNoSigs) -- | These launch parameters are used in a number of ways:+-- -- * Serializing the seed/script path for saves--- * Holding parse status from form fields, including Error info+-- * Holding parse status from form fields, including error info -- * Carrying fully-validated launch parameters. -- -- Type parameters are utilized to support all of these use cases.@@ -45,9 +49,9 @@ deriving instance FromJSON SerializableLaunchParams deriving instance ToJSON SerializableLaunchParams --- | A "ScenarioStatus" stores the status of a scenario along with--- appropriate metadata: "NotStarted", or "Played".--- The "Played" status has two sub-states: "Attempted" or "Completed".+-- | A 'ScenarioStatus' stores the status of a scenario along with+-- appropriate metadata: 'NotStarted', or 'Played'.+-- The 'Played' status has two sub-states: 'Attempted' or 'Completed'. data ScenarioStatus = NotStarted | Played@@ -63,14 +67,17 @@ toEncoding = genericToEncoding scenarioOptions toJSON = genericToJSON scenarioOptions +emptyLaunchParams :: Applicative f => ParameterizableLaunchParams a f+emptyLaunchParams = LaunchParams (pure Nothing) (pure Nothing)+ getLaunchParams :: ScenarioStatus -> SerializableLaunchParams getLaunchParams = \case- NotStarted -> LaunchParams (pure Nothing) (pure Nothing)+ NotStarted -> emptyLaunchParams Played x _ _ -> x --- | A "ScenarioInfo" record stores metadata about a scenario: its+-- | A 'ScenarioInfo' record stores metadata about a scenario: its -- canonical path and status.--- By way of the "ScenarioStatus" record, it stores the+-- By way of the 'ScenarioStatus' record, it stores the -- most recent status and best-ever status. data ScenarioInfo = ScenarioInfo { _scenarioPath :: FilePath@@ -95,11 +102,11 @@ -- | The status of the scenario. scenarioStatus :: Lens' ScenarioInfo ScenarioStatus --- | Update the current "ScenarioInfo" record when quitting a game.+-- | Update the current 'ScenarioInfo' record when quitting a game. -- -- Note that when comparing \"best\" times, shorter is not always better! -- As long as the scenario is not completed (e.g. some do not have win condition)--- we consider having fun _longer_ to be better.+-- we consider having fun /longer/ to be better. updateScenarioInfoOnFinish :: CodeSizeDeterminators -> ZonedTime ->
src/Swarm/Game/Scenario/Style.hs view
@@ -1,5 +1,7 @@ -- | -- SPDX-License-Identifier: BSD-3-Clause+--+-- Types for styling custom entity attributes module Swarm.Game.Scenario.Style where import Data.Aeson@@ -30,6 +32,8 @@ instance ToJSON StyleFlag where toJSON = genericToJSON styleFlagJsonOptions +-- | Hexadecimal color notation.+-- May include a leading hash symbol (see 'Data.Colour.SRGB.sRGB24read'). newtype HexColor = HexColor Text deriving (Eq, Show, Generic, FromJSON, ToJSON)
src/Swarm/Game/Scenario/Topography/Area.hs view
@@ -1,5 +1,7 @@ {-# LANGUAGE OverloadedStrings #-} +-- |+-- SPDX-License-Identifier: BSD-3-Clause module Swarm.Game.Scenario.Topography.Area where import Data.Int (Int32)@@ -8,6 +10,7 @@ import Linear (V2 (..)) import Swarm.Game.Location +-- | Height and width of a 2D map region data AreaDimensions = AreaDimensions { rectWidth :: Int32 , rectHeight :: Int32@@ -20,9 +23,9 @@ invertY :: V2 Int32 -> V2 Int32 invertY (V2 x y) = V2 x (-y) --- | Incorporates an offset by -1, since the area is+-- | Incorporates an offset by @-1@, since the area is -- "inclusive" of the lower-right coordinate.--- Inverse of "cornersToArea".+-- Inverse of 'cornersToArea'. upperLeftToBottomRight :: AreaDimensions -> Location -> Location upperLeftToBottomRight (AreaDimensions w h) upperLeft = upperLeft .+^ displacement@@ -30,18 +33,20 @@ displacement = invertY $ subtract 1 <$> V2 w h -- | Converts the displacement vector between the two--- diagonal corners of the rectangle into an "AreaDimensions" record.+-- diagonal corners of the rectangle into an 'AreaDimensions' record. -- Adds one to both dimensions since the corner coordinates are "inclusive".--- Inverse of "upperLeftToBottomRight".+-- Inverse of 'upperLeftToBottomRight'. cornersToArea :: Location -> Location -> AreaDimensions cornersToArea upperLeft lowerRight = AreaDimensions x y where V2 x y = (+ 1) <$> invertY (lowerRight .-. upperLeft) +-- | Has zero width or height. isEmpty :: AreaDimensions -> Bool isEmpty (AreaDimensions w h) = w == 0 || h == 0 +-- | Extracts the dimensions of a map grid. getAreaDimensions :: [[a]] -> AreaDimensions getAreaDimensions cellGrid = AreaDimensions w h
src/Swarm/Game/Scenario/Topography/Cell.hs view
@@ -11,8 +11,8 @@ ) where import Control.Lens hiding (from, (.=), (<.>))-import Control.Monad (when) import Control.Monad.Extra (mapMaybeM)+import Data.List.NonEmpty qualified as NE import Data.Maybe (catMaybes, listToMaybe) import Data.Text (Text) import Data.Vector qualified as V@@ -31,8 +31,8 @@ -- | 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.+-- 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 :: Erasable e@@ -51,7 +51,7 @@ } deriving (Eq, Show) --- | Re-usable serialization for variants of "PCell"+-- | Re-usable serialization for variants of 'PCell' mkPCellJson :: ToJSON b => (Erasable a -> Maybe b) -> PCell a -> Value mkPCellJson modifier x = toJSON $@@ -69,10 +69,12 @@ 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)"+ let tupRaw = V.toList v+ tup <- case NE.nonEmpty tupRaw of+ Nothing -> fail "palette entry must have nonzero length (terrain, optional entity and then robots if any)"+ Just x -> return x - terr <- liftE $ parseJSON (head tup)+ terr <- liftE $ parseJSON (NE.head tup) ent <- case tup ^? ix 1 of Nothing -> return ENothing@@ -87,7 +89,7 @@ mrName <- liftE $ parseJSON @(Maybe RobotName) r traverse (localE snd . getRobot) mrName - robs <- mapMaybeM name2rob (drop 2 tup)+ robs <- mapMaybeM name2rob (drop 2 tupRaw) return $ Cell terr ent robs @@ -115,7 +117,7 @@ -- for rendering. type CellPaintDisplay = PCell EntityFacade --- Note: This instance is used only for the purpose of WorldPalette+-- Note: This instance is used only for the purpose of 'WorldPalette' instance ToJSON CellPaintDisplay where toJSON = mkPCellJson $ \case ENothing -> Nothing
src/Swarm/Game/Scenario/Topography/EntityFacade.hs view
@@ -1,6 +1,9 @@ {-# LANGUAGE DerivingVia #-} --- | Stand-in type for an "Entity" for purposes+-- |+-- SPDX-License-Identifier: BSD-3-Clause+--+-- Stand-in type for an "Entity" for purposes -- that do not require carrying around the entire state -- of an Entity. --@@ -9,19 +12,16 @@ module Swarm.Game.Scenario.Topography.EntityFacade where import Control.Lens ((^.))-import Data.Text (Text) import Data.Yaml as Y (ToJSON (toJSON)) import Swarm.Game.Display (Display) import Swarm.Game.Entity qualified as E -type EntityName = Text- -- | This datatype is a lightweight stand-in for the -- full-fledged "Entity" type without the baggage of all -- of its other fields. -- It contains the bare minimum display information -- for rendering.-data EntityFacade = EntityFacade EntityName Display+data EntityFacade = EntityFacade E.EntityName Display deriving (Eq) -- Note: This instance is used only for the purpose of WorldPalette
@@ -3,6 +3,15 @@ -- | -- SPDX-License-Identifier: BSD-3-Clause+--+-- Type definitions and validation logic for portals.+--+-- Portals can be inter-world or intra-world.+-- It is legal for a portal exit to be on the same cell as its entrance.+--+-- By default, passage through a portal preserves the orientation+-- of the robot, but an extra portal parameter can specify+-- that the robot should be re-oriented. module Swarm.Game.Scenario.Topography.Navigation.Portal where import Control.Arrow ((&&&))@@ -40,12 +49,14 @@ -- | Parameterized on waypoint dimensionality ('additionalDimension') and -- on the portal location specification method ('portalExitLoc').+-- -- == @additionalDimension@--- As a member of the 'WorldDescription', waypoints are only known within a+-- As a member of the 'Swarm.Game.Scenario.Topography.WorldDescription.WorldDescription', waypoints are only known within a -- a single subworld, so 'additionalDimension' is 'Identity' for the map -- of waypoint names to planar locations. -- At the Scenario level, in contrast, we have access to all subworlds, so -- we nest this map to planar locations in additional mapping layer by subworld.+-- -- == @portalExitLoc@ -- At the subworld parsing level, we only can obtain the planar location -- for portal /entrances/, but the /exits/ remain as waypoint names.@@ -110,14 +121,15 @@ -- | -- The following constraints must be enforced:+-- -- * portals based on plural waypoint multiplicity can have multiple entrances but only a single exit -- * no two portals share the same entrance location -- * waypoint uniqueness within a subworld when the 'unique' flag is specified ----- == Data flow:+-- == Data flow -- -- Waypoints are defined within a subworld and are namespaced by it.--- Optional intra-subworld uniqueness of Waypoints is enforced at WorldDescription+-- Optional intra-subworld uniqueness of Waypoints is enforced at 'Swarm.Game.Scenario.Topography.WorldDescription.WorldDescription' -- parse time. -- Portals are declared within a subworld. The portal entrance must be a waypoint -- within this subworld.@@ -128,7 +140,7 @@ -- no entrances overlap can also be performed at that level. -- * However, enforcement of single-multiplicity on portal /exits/ must be performed -- at scenario-parse level, because for a portal exit that references a waypoint in--- another subworld, we can't know at the single-WorldDescription level whether+-- another subworld, we can't know at the single-'Swarm.Game.Scenario.Topography.WorldDescription.WorldDescription' level whether -- that waypoint has plural multiplicity. validatePartialNavigation :: (MonadFail m, Traversable t) =>@@ -217,6 +229,7 @@ -- -- Verifying this is simple: -- For all of the portals between Subworlds A and B:+-- -- * The coordinates of all \"consistent\" portal locations in Subworld A -- are subtracted from the corresponding coordinates in Subworld B. It -- does not matter which are exits vs. entrances.@@ -271,6 +284,7 @@ -- -- == Discussion -- Compare to the 'Traversable' instance of 'Signed':+-- -- @ -- instance Traversable Signed where -- traverse f (Positive x) = Positive <$> f x@@ -278,6 +292,7 @@ -- @ -- -- if we were to substitute 'id' for f:+-- -- @ -- traverse id (Positive x) = Positive <$> id x -- traverse id (Negative x) = Negative <$> id x
@@ -0,0 +1,44 @@+-- |+-- SPDX-License-Identifier: BSD-3-Clause+module Swarm.Game.Scenario.Topography.Navigation.Util where++import Control.Lens (view)+import Data.Function (on)+import Data.Int (Int32)+import Linear (V2)+import Swarm.Game.Location+import Swarm.Game.Robot+import Swarm.Game.Universe (Cosmic, planar)+import Swarm.Language.Direction++-- |+-- Computes the relative offset vector between a 'Robot' and a 'Location'+-- (presumed to be in the same subworld, though the contrary will+-- not result in failure), then re-interpret that vector based on the+-- 'Robot'\'s current orientation.+--+-- If the robot is not oriented in a cardinal direction, returns 'Nothing'.+--+-- = Re-orientation semantics+--+-- Given a displacement vector @(x, y)@ where:+--+-- * positive @x@-coordinate represents @east@+-- * negative @x@-coordinate represents @west@+-- * positive @y@-coordinate represents @north@+-- * negative @y@-coordinate represents @south@+--+-- the re-interpreted vector @(x', y')@ becomes:+--+-- * positive @x'@-coordinate represents @right@+-- * negative @x'@-coordinate represents @left@+-- * positive @y'@-coordinate represents @forward@+-- * negative @y'@-coordinate represents @back@+orientationBasedRelativePosition :: Robot -> Cosmic Location -> Maybe (V2 Int32)+orientationBasedRelativePosition selfRobot otherLocation =+ (`applyTurn` relativeCoords) <$> maybeSelfDirRelativeToNorth+ where+ maybeSelfDirection = view robotOrientation selfRobot >>= toAbsDirection+ maybeSelfDirRelativeToNorth = DRelative . DPlanar . relativeTo DNorth <$> maybeSelfDirection++ relativeCoords = ((.-.) `on` view planar) otherLocation (view robotLocation selfRobot)
@@ -2,6 +2,22 @@ -- | -- SPDX-License-Identifier: BSD-3-Clause+--+-- Landmarks that are used to specify portal locations+-- and can serve as navigation aids via the `waypoint` command.+--+-- = Waypoint ordering+--+-- The sequence of waypoints of a given name is dictated by criteria in the following order:+--+-- 1. Ordering of structure placements+-- (see implementation of 'Swarm.Game.Scenario.Topography.Structure.mergeStructures');+-- later placements are ordered first.+-- 2. Placement of cells within a map. Map locations go by row-major order+-- (compare to docs for 'Swarm.Game.State.genRobotTemplates').+--+-- TODO (#1366): May be useful to have a mechanism for more+-- precise control of ordering. module Swarm.Game.Scenario.Topography.Navigation.Waypoint where import Data.Int (Int32)
src/Swarm/Game/Scenario/Topography/Placement.hs view
@@ -2,6 +2,9 @@ -- | -- SPDX-License-Identifier: BSD-3-Clause+--+-- Descriptions of the orientation and offset by+-- which a structure should be placed. module Swarm.Game.Scenario.Topography.Placement where import Data.List (transpose)
src/Swarm/Game/Scenario/Topography/Structure.hs view
@@ -2,6 +2,9 @@ -- | -- SPDX-License-Identifier: BSD-3-Clause+--+-- Definitions of "structures" for use within a map,+-- as well as logic for combining them. module Swarm.Game.Scenario.Topography.Structure where import Control.Applicative ((<|>))@@ -94,7 +97,7 @@ else drop $ abs integralOffset -- | Overlays all of the "child placements", such that the children encountered earlier--- in the YAML file supersede the later ones (due to use of "foldr" instead of "foldl").+-- in the YAML file supersede the later ones (due to use of 'foldr' instead of 'foldl'). mergeStructures :: M.Map StructureName (PStructure (Maybe a)) -> Maybe Placement ->@@ -121,8 +124,8 @@ (maskedArea, mapWaypoints) <- liftE $ (v .:? "map" .!= "") >>= paintMap maybeMaskChar pal return $ Structure maskedArea localStructureDefs placementDefs $ waypointDefs <> mapWaypoints --- | "Paint" a world map using a 'WorldPalette', turning it from a raw--- string into a nested list of 'Cell' values by looking up each+-- | \"Paint\" a world map using a 'WorldPalette', turning it from a raw+-- string into a nested list of 'PCell' values by looking up each -- character in the palette, failing if any character in the raw map -- is not contained in the palette. paintMap ::
src/Swarm/Game/ScenarioInfo.hs view
@@ -75,7 +75,7 @@ -- ---------------------------------------------------------------------------- -- | A scenario item is either a specific scenario, or a collection of--- scenarios (*e.g.* the scenarios contained in a subdirectory).+-- scenarios (/e.g./ the scenarios contained in a subdirectory). data ScenarioItem = SISingle ScenarioInfoPair | SICollection Text ScenarioCollection deriving (Show) @@ -85,15 +85,17 @@ scenarioItemName (SICollection name _) = name -- | A scenario collection is a tree of scenarios, keyed by name,--- together with an optional order. Invariant: every item in the--- scOrder exists as a key in the scMap.+-- together with an optional order.+--+-- /Invariant:/ every item in the+-- 'scOrder' exists as a key in the 'scMap'. data ScenarioCollection = SC { scOrder :: Maybe [FilePath] , scMap :: Map FilePath ScenarioItem } deriving (Show) --- | Access and modify ScenarioItems in collection based on their path.+-- | Access and modify 'ScenarioItem's in collection based on their path. scenarioItemByPath :: FilePath -> Traversal' ScenarioCollection ScenarioItem scenarioItemByPath path = ixp ps where
src/Swarm/Game/State.hs view
@@ -29,13 +29,8 @@ -- ** GameState fields creativeMode,- gameStep, winCondition, winSolution,- gameAchievements,- announcementQueue,- runStatus,- paused, robotMap, robotsByLocation, robotsAtLocation,@@ -44,39 +39,75 @@ baseRobot, activeRobots, waitingRobots,- availableRecipes,- availableCommands, messageNotifications,- allDiscoveredEntities,- gensym, seed, randGen,- adjList,- nameList,- initiallyRunCode,- entityMap,- recipesOut,- recipesIn,- recipesReq, currentScenarioPath,- knownEntities,- worldNavigation,- multiWorld,- worldScrollable, viewCenterRule, viewCenter, needsRedraw,+ focusedRobotID,++ -- *** Subrecord accessors+ temporal,+ robotNaming,+ recipesInfo,+ messageInfo,+ gameControls,+ discovery,+ landscape,++ -- ** GameState subrecords++ -- *** Temporal state+ TemporalState,+ gameStep,+ runStatus,+ ticks,+ robotStepsPerTick,+ paused,++ -- *** Robot naming+ RobotNaming,+ nameGenerator,+ gensym,++ -- *** Recipes+ Recipes,+ recipesOut,+ recipesIn,+ recipesCat,++ -- *** Messages+ Messages,+ messageQueue,+ lastSeenMessageTime,+ announcementQueue,++ -- *** Controls+ GameControls,+ initiallyRunCode, replStatus, replNextValueIndex, replWorking, replActiveType, inputHandler,- messageQueue,- lastSeenMessageTime,- focusedRobotID,- ticks,- robotStepsPerTick, + -- *** Discovery+ Discovery,+ allDiscoveredEntities,+ availableRecipes,+ availableCommands,+ knownEntities,+ gameAchievements,++ -- *** Landscape+ Landscape,+ worldNavigation,+ multiWorld,+ worldScrollable,+ entityMap,+ -- ** Notifications Notifications (..), notificationsCount,@@ -104,6 +135,7 @@ focusedRobot, RobotRange (..), focusedRange,+ getRadioRange, clearFocusedRobotLogUpdated, addRobot, addRobotToLocation,@@ -120,6 +152,9 @@ messageIsRecent, messageIsFromNearby, getRunCodePath,+ buildWorldTuples,+ genMultiWorld,+ genRobotTemplates, ) where import Control.Applicative ((<|>))@@ -146,7 +181,7 @@ import Data.List.NonEmpty qualified as NE import Data.Map (Map) import Data.Map qualified as M-import Data.Maybe (fromMaybe, isJust, isNothing, mapMaybe)+import Data.Maybe (fromMaybe, isJust, isNothing, listToMaybe, mapMaybe) import Data.Sequence (Seq ((:<|))) import Data.Sequence qualified as Seq import Data.Set qualified as S@@ -167,10 +202,11 @@ import Swarm.Game.Location import Swarm.Game.Recipe ( Recipe,+ catRecipeMap, inRecipeMap, outRecipeMap,- reqRecipeMap, )+import Swarm.Game.ResourceLoading (NameGenerator) import Swarm.Game.Robot import Swarm.Game.Scenario.Objective import Swarm.Game.Scenario.Status@@ -191,9 +227,10 @@ import Swarm.Language.Typed (Typed (Typed)) import Swarm.Language.Types import Swarm.Language.Value (Value)+import Swarm.Log import Swarm.Util (applyWhen, binTuples, surfaceEmpty, uniq, (<+=), (<<.=), (?)) import Swarm.Util.Erasable-import Swarm.Util.Lens (makeLensesExcluding)+import Swarm.Util.Lens (makeLensesExcluding, makeLensesNoSigs) import System.Clock qualified as Clock import System.Random (StdGen, mkStdGen, randomRIO) @@ -216,11 +253,12 @@ 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.+ --+ -- INVARIANT: the 'Value' stored here is not a 'Swarm.Language.Value.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- -- entered. The @Maybe Value@ starts out as @Nothing@ and gets+ -- entered. The @Maybe Value@ starts out as 'Nothing' and gets -- filled in with a result once the command completes. REPLWorking (Typed (Maybe Value)) deriving (Eq, Show, Generic, FromJSON, ToJSON)@@ -252,7 +290,11 @@ makePrisms ''WinCondition instance ToSample WinCondition where- toSamples _ = SD.noSamples+ toSamples _ =+ SD.samples+ [ NoWinCondition+ -- TODO: #1552 add simple objective sample+ ] -- | A data type to keep track of the pause mode. data RunStatus@@ -268,11 +310,13 @@ -- | Switch (auto or manually) paused game to running and running to manually paused. -- -- Note that this function is not safe to use in the app directly, because the UI--- also tracks time between ticks - use 'Swarm.TUI.Controller.safeTogglePause' instead.+-- also tracks time between ticks---use 'Swarm.TUI.Controller.safeTogglePause' instead. toggleRunStatus :: RunStatus -> RunStatus toggleRunStatus s = if s == Running then ManualPause else Running --- | A data type to keep track of discovered recipes and commands+-- | A data type to keep track of some kind of log or sequence, with+-- an index to remember which ones are "new" and which ones have+-- "already been seen". data Notifications a = Notifications { _notificationsCount :: Int , _notificationsContent :: [a]@@ -329,9 +373,9 @@ defaultRobotStepsPerTick :: Int defaultRobotStepsPerTick = 100 --- | Type for remebering which robots will be run next in a robot step mode.+-- | Type for remembering which robots will be run next in a robot step mode. ----- Once some robots have run, we need to store RID to know which ones should go next.+-- Once some robots have run, we need to store 'RID' to know which ones should go next. -- At 'SBefore' no robots were run yet, so it is safe to transition to and from 'WorldTick'. -- -- @@@ -361,21 +405,171 @@ -- | Game step mode - we use the single step mode when debugging robot 'CESK' machine. data Step = WorldTick | RobotStep SingleStep +data Recipes = Recipes+ { _recipesOut :: IntMap [Recipe Entity]+ , _recipesIn :: IntMap [Recipe Entity]+ , _recipesCat :: IntMap [Recipe Entity]+ }++makeLensesNoSigs ''Recipes++-- | All recipes the game knows about, indexed by outputs.+recipesOut :: Lens' Recipes (IntMap [Recipe Entity])++-- | All recipes the game knows about, indexed by inputs.+recipesIn :: Lens' Recipes (IntMap [Recipe Entity])++-- | All recipes the game knows about, indexed by requirement/catalyst.+recipesCat :: Lens' Recipes (IntMap [Recipe Entity])++data Messages = Messages+ { _messageQueue :: Seq LogEntry+ , _lastSeenMessageTime :: TickNumber+ , _announcementQueue :: Seq Announcement+ }++makeLensesNoSigs ''Messages++-- | A queue of global messages.+--+-- Note that we put the newest entry to the right.+messageQueue :: Lens' Messages (Seq LogEntry)++-- | Last time message queue has been viewed (used for notification).+lastSeenMessageTime :: Lens' Messages TickNumber++-- | A queue of global announcements.+-- 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' Messages (Seq Announcement)++data RobotNaming = RobotNaming+ { _nameGenerator :: NameGenerator+ , _gensym :: Int+ }++makeLensesExcluding ['_nameGenerator] ''RobotNaming++--- | Read-only list of words, for use in building random robot names.+nameGenerator :: Getter RobotNaming NameGenerator+nameGenerator = to _nameGenerator++-- | A counter used to generate globally unique IDs.+gensym :: Lens' RobotNaming Int++data TemporalState = TemporalState+ { _gameStep :: Step+ , _runStatus :: RunStatus+ , _ticks :: TickNumber+ , _robotStepsPerTick :: Int+ }++makeLensesNoSigs ''TemporalState++-- | How to step the game: 'WorldTick' or 'RobotStep' for debugging the 'CESK' machine.+gameStep :: Lens' TemporalState Step++-- | The current 'RunStatus'.+runStatus :: Lens' TemporalState RunStatus++-- | Whether the game is currently paused.+paused :: Getter TemporalState Bool+paused = to (\s -> s ^. runStatus /= Running)++-- | The number of ticks elapsed since the game started.+ticks :: Lens' TemporalState TickNumber++-- | The maximum number of CESK machine steps a robot may take during+-- a single tick.+robotStepsPerTick :: Lens' TemporalState Int++data GameControls = GameControls+ { _replStatus :: REPLStatus+ , _replNextValueIndex :: Integer+ , _inputHandler :: Maybe (Text, Value)+ , _initiallyRunCode :: Maybe ProcessedTerm+ }++makeLensesNoSigs ''GameControls++-- | The current status of the REPL.+replStatus :: Lens' GameControls REPLStatus++-- | The index of the next @it{index}@ value+replNextValueIndex :: Lens' GameControls Integer++-- | The currently installed input handler and hint text.+inputHandler :: Lens' GameControls (Maybe (Text, Value))++-- | Code that is run upon scenario start, before any+-- REPL interaction.+initiallyRunCode :: Lens' GameControls (Maybe ProcessedTerm)++data Discovery = Discovery+ { _allDiscoveredEntities :: Inventory+ , _availableRecipes :: Notifications (Recipe Entity)+ , _availableCommands :: Notifications Const+ , _knownEntities :: [Text]+ , _gameAchievements :: Map GameplayAchievement Attainment+ }++makeLensesNoSigs ''Discovery++-- | The list of entities that have been discovered.+allDiscoveredEntities :: Lens' Discovery Inventory++-- | The list of available recipes.+availableRecipes :: Lens' Discovery (Notifications (Recipe Entity))++-- | The list of available commands.+availableCommands :: Lens' Discovery (Notifications Const)++-- | The names of entities that should be considered \"known\", that is,+-- robots know what they are without having to scan them.+knownEntities :: Lens' Discovery [Text]++-- | Map of in-game achievements that were obtained+gameAchievements :: Lens' Discovery (Map GameplayAchievement Attainment)++data Landscape = Landscape+ { _worldNavigation :: Navigation (M.Map SubworldName) Location+ , _multiWorld :: W.MultiWorld Int Entity+ , _entityMap :: EntityMap+ , _worldScrollable :: Bool+ }++makeLensesNoSigs ''Landscape++-- | Includes a 'Map' of named locations and an+-- "edge list" (graph) that maps portal entrances to exits+worldNavigation :: Lens' Landscape (Navigation (M.Map SubworldName) Location)++-- | The current state of the world (terrain and entities only; robots+-- are stored in the 'robotMap'). 'Int' is used instead of+-- 'TerrainType' because we need to be able to store terrain values in+-- unboxed tile arrays.+multiWorld :: Lens' Landscape (W.MultiWorld Int Entity)++-- | The catalog of all entities that the game knows about.+entityMap :: Lens' Landscape EntityMap++-- | Whether the world map is supposed to be scrollable or not.+worldScrollable :: Lens' Landscape Bool+ -- | The main record holding the state for the game itself (as -- distinct from the UI). See the lenses below for access to its -- fields. data GameState = GameState { _creativeMode :: Bool- , _gameStep :: Step+ , _temporal :: TemporalState , _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- -- be a subset of the keys of robotMap. It may contain waiting or idle- -- robots. But robots that are present in robotMap and not in activeRobots+ -- be a subset of the keys of 'robotMap'. It may contain waiting or idle+ -- robots. But robots that are present in 'robotMap' and not in 'activeRobots' -- are guaranteed to be either waiting or idle. _activeRobots :: IntSet , -- A set of probably waiting robots, indexed by probable wake-up time. It@@ -383,44 +577,28 @@ -- that do not exist anymore. Its only guarantee is that once a robot name -- with its wake up time is inserted in it, it will remain there until the -- wake-up time is reached, at which point it is removed via- -- wakeUpRobotsDoneSleeping.+ -- 'wakeUpRobotsDoneSleeping'. -- Waiting robots for a given time are a list because it is cheaper to- -- prepend to a list than insert into a Set.+ -- prepend to a list than insert into a 'Set'. _waitingRobots :: Map TickNumber [RID] , _robotsByLocation :: Map SubworldName (Map Location IntSet) , -- This member exists as an optimization so -- that we do not have to iterate over all "waiting" robots, -- since there may be many. _robotsWatching :: Map (Cosmic Location) (S.Set RID)- , _allDiscoveredEntities :: Inventory- , _availableRecipes :: Notifications (Recipe Entity)- , _availableCommands :: Notifications Const- , _gensym :: Int+ , _discovery :: Discovery , _seed :: Seed , _randGen :: StdGen- , _adjList :: Array Int Text- , _nameList :: Array Int Text- , _initiallyRunCode :: Maybe ProcessedTerm- , _entityMap :: EntityMap- , _recipesOut :: IntMap [Recipe Entity]- , _recipesIn :: IntMap [Recipe Entity]- , _recipesReq :: IntMap [Recipe Entity]+ , _robotNaming :: RobotNaming+ , _recipesInfo :: Recipes , _currentScenarioPath :: Maybe FilePath- , _knownEntities :: [Text]- , _worldNavigation :: Navigation (M.Map SubworldName) Location- , _multiWorld :: W.MultiWorld Int Entity- , _worldScrollable :: Bool+ , _landscape :: Landscape , _viewCenterRule :: ViewCenterRule , _viewCenter :: Cosmic Location , _needsRedraw :: Bool- , _replStatus :: REPLStatus- , _replNextValueIndex :: Integer- , _inputHandler :: Maybe (Text, Value)- , _messageQueue :: Seq LogEntry- , _lastSeenMessageTime :: TickNumber+ , _gameControls :: GameControls+ , _messageInfo :: Messages , _focusedRobotID :: RID- , _ticks :: TickNumber- , _robotStepsPerTick :: Int } ------------------------------------------------------------@@ -435,13 +613,13 @@ ] ''GameState -makeLensesExcluding ['_viewCenter, '_focusedRobotID, '_viewCenterRule, '_activeRobots, '_waitingRobots, '_adjList, '_nameList] ''GameState+makeLensesExcluding ['_viewCenter, '_focusedRobotID, '_viewCenterRule, '_activeRobots, '_waitingRobots] ''GameState -- | Is the user in creative mode (i.e. able to do anything without restriction)? creativeMode :: Lens' GameState Bool --- | How to step the game - 'WorldTick' or 'RobotStep' for debugging the 'CESK' machine.-gameStep :: Lens' GameState Step+-- | Aspects of the temporal state of the game+temporal :: Lens' GameState TemporalState -- | How to determine whether the player has won. winCondition :: Lens' GameState WinCondition@@ -450,28 +628,11 @@ -- 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 announcements.--- 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---- | Whether the game is currently paused.-paused :: Getter GameState Bool-paused = to (\s -> s ^. runStatus /= Running)- -- | 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--- location (which we need both for /e.g./ the 'Salvage' command as+-- location (which we need both for /e.g./ the @salvage@ command as -- well as for actually drawing the world). Unfortunately there is -- no good way to automatically keep this up to date, since we don't -- just want to completely rebuild it every time the 'robotMap'@@ -510,27 +671,18 @@ baseRobot :: Traversal' GameState Robot baseRobot = robotMap . ix 0 --- | The list of entities that have been discovered.-allDiscoveredEntities :: Lens' GameState Inventory---- | The list of available recipes.-availableRecipes :: Lens' GameState (Notifications (Recipe Entity))---- | The list of available commands.-availableCommands :: Lens' GameState (Notifications Const)- -- | The names of the robots that are currently not sleeping. activeRobots :: Getter GameState IntSet activeRobots = internalActiveRobots -- | The names of the robots that are currently sleeping, indexed by wake up -- time. Note that this may not include all sleeping robots, particularly--- those that are only taking a short nap (e.g. wait 1).+-- those that are only taking a short nap (e.g. @wait 1@). waitingRobots :: Getter GameState (Map TickNumber [RID]) waitingRobots = internalWaitingRobots --- | A counter used to generate globally unique IDs.-gensym :: Lens' GameState Int+-- | Discovery state of entities, commands, recipes+discovery :: Lens' GameState Discovery -- | The initial seed that was used for the random number generator, -- and world generation.@@ -539,52 +691,20 @@ -- | Pseudorandom generator initialized at start. randGen :: Lens' GameState StdGen --- | Read-only list of words, for use in building random robot names.-adjList :: Getter GameState (Array Int Text)-adjList = to _adjList---- | Read-only list of words, for use in building random robot names.-nameList :: Getter GameState (Array Int Text)-nameList = to _nameList---- | Code that is run upon scenario start, before any--- REPL interaction.-initiallyRunCode :: Lens' GameState (Maybe ProcessedTerm)---- | The catalog of all entities that the game knows about.-entityMap :: Lens' GameState EntityMap---- | All recipes the game knows about, indexed by outputs.-recipesOut :: Lens' GameState (IntMap [Recipe Entity])---- | All recipes the game knows about, indexed by inputs.-recipesIn :: Lens' GameState (IntMap [Recipe Entity])+-- | State and data for assigning identifiers to robots+robotNaming :: Lens' GameState RobotNaming --- | All recipes the game knows about, indexed by requirement/catalyst.-recipesReq :: Lens' GameState (IntMap [Recipe Entity])+-- | Collection of recipe info+recipesInfo :: Lens' GameState Recipes -- | The filepath of the currently running scenario. ----- This is useful as an index to 'scenarios' collection,+-- This is useful as an index to the scenarios collection, -- see 'Swarm.Game.ScenarioInfo.scenarioItemByPath'. currentScenarioPath :: Lens' GameState (Maybe FilePath) --- | The names of entities that should be considered \"known\", that is,--- robots know what they are without having to scan them.-knownEntities :: Lens' GameState [Text]---- | Includes a 'Map' of named locations and an--- "Edge list" (graph) that maps portal entrances to exits-worldNavigation :: Lens' GameState (Navigation (M.Map SubworldName) Location)---- | The current state of the world (terrain and entities only; robots--- are stored in the 'robotMap'). 'Int' is used instead of--- 'TerrainType' because we need to be able to store terrain values in--- unboxed tile arrays.-multiWorld :: Lens' GameState (W.MultiWorld Int Entity)---- | Whether the world map is supposed to be scrollable or not.-worldScrollable :: Lens' GameState Bool+-- | Info about the lay of the land+landscape :: Lens' GameState Landscape -- | The current center of the world view. Note that this cannot be -- modified directly, since it is calculated automatically from the@@ -596,22 +716,11 @@ -- | Whether the world view needs to be redrawn. needsRedraw :: Lens' GameState Bool --- | The current status of the REPL.-replStatus :: Lens' GameState REPLStatus---- | The index of the next it{index} value-replNextValueIndex :: Lens' GameState Integer---- | The currently installed input handler and hint text.-inputHandler :: Lens' GameState (Maybe (Text, Value))---- | A queue of global messages.------ Note that we put the newest entry to the right.-messageQueue :: Lens' GameState (Seq LogEntry)+-- | Controls, including REPL and key mapping+gameControls :: Lens' GameState GameControls --- | Last time message queue has been viewed (used for notification).-lastSeenMessageTime :: Lens' GameState TickNumber+-- | Message info+messageInfo :: Lens' GameState Messages -- | The current robot in focus. --@@ -623,27 +732,20 @@ focusedRobotID :: Getter GameState RID focusedRobotID = to _focusedRobotID --- | The number of ticks elapsed since the game started.-ticks :: Lens' GameState TickNumber---- | The maximum number of CESK machine steps a robot may take during--- a single tick.-robotStepsPerTick :: Lens' GameState Int- ------------------------------------------------------------ -- Utilities ------------------------------------------------------------ -- | The current rule for determining the center of the world view.--- It updates also, viewCenter and focusedRobotName to keep--- everything synchronize.+-- It updates also, 'viewCenter' and 'focusedRobot' to keep+-- everything synchronized. viewCenterRule :: Lens' GameState ViewCenterRule viewCenterRule = lens getter setter where getter :: GameState -> ViewCenterRule getter = _viewCenterRule - -- The setter takes care of updating viewCenter and focusedRobotName+ -- The setter takes care of updating 'viewCenter' and 'focusedRobot' -- So non of this fields get out of sync. setter :: GameState -> ViewCenterRule -> GameState setter g rule =@@ -658,7 +760,7 @@ Just loc -> g {_viewCenterRule = rule, _viewCenter = loc, _focusedRobotID = rid} -- | Whether the repl is currently working.-replWorking :: Getter GameState Bool+replWorking :: Getter GameControls Bool replWorking = to (\s -> matchesWorking $ s ^. replStatus) where matchesWorking (REPLDone _) = False@@ -679,29 +781,33 @@ getNotif gs = Notifications {_notificationsCount = length new, _notificationsContent = allUniq} where allUniq = uniq $ toList allMessages- new = takeWhile (\l -> l ^. leTime > gs ^. lastSeenMessageTime) $ reverse allUniq+ new = takeWhile (\l -> l ^. leTime > gs ^. messageInfo . lastSeenMessageTime) $ reverse allUniq -- creative players and system robots just see all messages (and focused robots logs) unchecked = gs ^. creativeMode || fromMaybe False (focusedRobot gs ^? _Just . systemRobot)- messages = (if unchecked then id else focusedOrLatestClose) (gs ^. messageQueue)+ messages = (if unchecked then id else focusedOrLatestClose) (gs ^. messageInfo . messageQueue) allMessages = Seq.sort $ focusedLogs <> messages focusedLogs = maybe Empty (view robotLog) (focusedRobot gs) -- classic players only get to see messages that they said and a one message that they just heard -- other they have to get from log latestMsg = messageIsRecent gs closeMsg = messageIsFromNearby (gs ^. viewCenter)+ generatedBy rid logEntry = case logEntry ^. leSource of+ RobotLog _ rid' _ -> rid == rid'+ _ -> False+ focusedOrLatestClose mq = (Seq.take 1 . Seq.reverse . Seq.filter closeMsg $ Seq.takeWhileR latestMsg mq)- <> Seq.filter ((== gs ^. focusedRobotID) . view leRobotID) mq+ <> Seq.filter (generatedBy (gs ^. focusedRobotID)) mq messageIsRecent :: GameState -> LogEntry -> Bool-messageIsRecent gs e = addTicks 1 (e ^. leTime) >= gs ^. ticks+messageIsRecent gs e = addTicks 1 (e ^. leTime) >= gs ^. temporal . ticks -- | Reconciles the possibilities of log messages being--- omnipresent and robots being in different worlds+-- omnipresent and robots being in different worlds messageIsFromNearby :: Cosmic Location -> LogEntry -> Bool-messageIsFromNearby l e = case e ^. leLocation of- Omnipresent -> True- Located x -> f x+messageIsFromNearby l e = case e ^. leSource of+ SystemLog -> True+ RobotLog _ _ loc -> f loc where f logLoc = case cosmoMeasure manhattan l logLoc of InfinitelyFar -> False@@ -715,7 +821,7 @@ applyViewCenterRule (VCLocation l) _ = Just l applyViewCenterRule (VCRobot name) m = m ^? at name . _Just . robotLocation --- | Recalculate the veiw center (and cache the result in the+-- | Recalculate the view center (and cache the result in the -- 'viewCenter' field) based on the current 'viewCenterRule'. If -- the 'viewCenterRule' specifies a robot which does not exist, -- simply leave the current 'viewCenter' as it is. Set 'needsRedraw'@@ -752,10 +858,10 @@ -- | Given a width and height, compute the region, centered on the -- 'viewCenter', that should currently be in view.-viewingRegion :: GameState -> (Int32, Int32) -> Cosmic W.BoundsRectangle-viewingRegion g (w, h) = Cosmic sw (W.Coords (rmin, cmin), W.Coords (rmax, cmax))+viewingRegion :: Cosmic Location -> (Int32, Int32) -> Cosmic W.BoundsRectangle+viewingRegion (Cosmic sw (Location cx cy)) (w, h) =+ Cosmic sw (W.Coords (rmin, cmin), W.Coords (rmax, cmax)) where- Cosmic sw (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) @@ -782,40 +888,54 @@ -- * If we are in creative or scroll-enabled mode, the focused robot is -- always considered 'Close'. -- * Otherwise, there is a "minimum radius" and "maximum radius".--- - If the robot is within the minimum radius, it is 'Close'.--- - If the robot is between the minimum and maximum radii, it+--+-- * If the robot is within the minimum radius, it is 'Close'.+-- * If the robot is between the minimum and maximum radii, it -- is 'MidRange', with a 'Double' value ranging linearly from -- 0 to 1 proportional to the distance from the minimum to--- maximum radius. For example, 'MidRange 0.5' would indicate+-- maximum radius. For example, @MidRange 0.5@ would indicate -- a robot exactly halfway between the minimum and maximum -- radii.--- - If the robot is beyond the maximum radius, it is 'Far'.+-- * If the robot is beyond the maximum radius, it is 'Far'.+-- -- * By default, the minimum radius is 16, and maximum is 64.--- * If the focused robot has an @antenna@ installed, it doubles--- both radii.--- * If the base has an @antenna@ installed, it also doubles both radii.+-- * Device augmentations+--+-- * If the focused robot has an @antenna@ installed, it doubles+-- both radii.+-- * If the base has an @antenna@ installed, it also doubles both radii. focusedRange :: GameState -> Maybe RobotRange-focusedRange g = checkRange <$ focusedRobot g+focusedRange g = checkRange <$ maybeFocusedRobot where+ maybeBaseRobot = g ^. robotMap . at 0+ maybeFocusedRobot = focusedRobot g+ checkRange = case r of InfinitelyFar -> Far Measurable r' -> computedRange r' computedRange r'- | g ^. creativeMode || g ^. worldScrollable || r' <= minRadius = Close+ | g ^. creativeMode || g ^. landscape . worldScrollable || r' <= minRadius = Close | r' > maxRadius = Far | otherwise = MidRange $ (r' - minRadius) / (maxRadius - minRadius) -- Euclidean distance from the base to the view center.- r = case g ^. robotMap . at 0 of+ r = case maybeBaseRobot of -- if the base doesn't exist, we have bigger problems Nothing -> InfinitelyFar Just br -> cosmoMeasure euclidean (g ^. viewCenter) (br ^. robotLocation) + (minRadius, maxRadius) = getRadioRange maybeBaseRobot maybeFocusedRobot++-- | Get the min/max communication radii given possible augmentations on each end+getRadioRange :: Maybe Robot -> Maybe Robot -> (Double, Double)+getRadioRange maybeBaseRobot maybeTargetRobot =+ (minRadius, maxRadius)+ where -- See whether the base or focused robot have antennas installed. baseInv, focInv :: Maybe Inventory- baseInv = g ^? robotMap . ix 0 . equippedDevices- focInv = view equippedDevices <$> focusedRobot g+ baseInv = view equippedDevices <$> maybeBaseRobot+ focInv = view equippedDevices <$> maybeTargetRobot gain :: Maybe Inventory -> (Double -> Double) gain (Just inv)@@ -834,12 +954,12 @@ robotMap . ix n . robotLogUpdated .= False -- | Add a concrete instance of a robot template to the game state:--- first, generate a unique ID number for it. Then, add it to the+-- First, generate a unique ID number for it. Then, add it to the -- main robot map, the active robot set, and to to the index of -- robots by location. Return the updated robot. addTRobot :: (Has (State GameState) sig m) => TRobot -> m Robot addTRobot r = do- rid <- gensym <+= 1+ rid <- robotNaming . gensym <+= 1 let r' = instantiateRobot rid r addRobot r' return r'@@ -869,33 +989,33 @@ -- | Add a message to the message queue. emitMessage :: (Has (State GameState) sig m) => LogEntry -> m ()-emitMessage msg = messageQueue %= (|> msg) . dropLastIfLong+emitMessage msg = messageInfo . messageQueue %= (|> msg) . dropLastIfLong where tooLong s = Seq.length s >= maxMessageQueueSize dropLastIfLong whole@(_oldest :<| newer) = if tooLong whole then newer else whole dropLastIfLong emptyQueue = emptyQueue --- | Takes a robot out of the activeRobots set and puts it in the waitingRobots+-- | Takes a robot out of the 'activeRobots' set and puts it in the 'waitingRobots' -- queue. sleepUntil :: (Has (State GameState) sig m) => RID -> TickNumber -> m () sleepUntil rid time = do internalActiveRobots %= IS.delete rid internalWaitingRobots . at time . non [] %= (rid :) --- | Takes a robot out of the activeRobots set.+-- | Takes a robot out of the 'activeRobots' set. sleepForever :: (Has (State GameState) sig m) => RID -> m () sleepForever rid = internalActiveRobots %= IS.delete rid --- | Adds a robot to the activeRobots set.+-- | Adds a robot to the 'activeRobots' set. activateRobot :: (Has (State GameState) sig m) => RID -> m () activateRobot rid = internalActiveRobots %= IS.insert rid -- | Removes robots whose wake up time matches the current game ticks count--- from the waitingRobots queue and put them back in the activeRobots set--- if they still exist in the keys of robotMap.+-- from the 'waitingRobots' queue and put them back in the 'activeRobots' set+-- if they still exist in the keys of 'robotMap'. wakeUpRobotsDoneSleeping :: (Has (State GameState) sig m) => m () wakeUpRobotsDoneSleeping = do- time <- use ticks+ time <- use $ temporal . ticks mrids <- internalWaitingRobots . at time <<.= Nothing case mrids of Nothing -> return ()@@ -905,7 +1025,7 @@ internalActiveRobots %= IS.union (IS.fromList aliveRids) -- These robots' wake times may have been moved "forward"- -- by "wakeWatchingRobots".+ -- by 'wakeWatchingRobots'. clearWatchingRobots rids -- | Clear the "watch" state of all of the@@ -917,14 +1037,14 @@ clearWatchingRobots rids = do robotsWatching %= M.map (`S.difference` S.fromList rids) --- | Iterates through all of the currently "wait"-ing robots,--- and moves forward the wake time of the ones that are watching this location.+-- | Iterates through all of the currently @wait@-ing robots,+-- and moves forward the wake time of the ones that are @watch@-ing this location. ----- NOTE: Clearing "TickNumber" map entries from "internalWaitingRobots"--- upon wakeup is handled by "wakeUpRobotsDoneSleeping" in State.hs+-- NOTE: Clearing 'TickNumber' map entries from 'internalWaitingRobots'+-- upon wakeup is handled by 'wakeUpRobotsDoneSleeping' wakeWatchingRobots :: (Has (State GameState) sig m) => Cosmic Location -> m () wakeWatchingRobots loc = do- currentTick <- use ticks+ currentTick <- use $ temporal . ticks waitingMap <- use waitingRobots rMap <- use robotMap watchingMap <- use robotsWatching@@ -1014,8 +1134,7 @@ -- | Record to pass information needed to create an initial -- 'GameState' record when starting a scenario. data GameStateConfig = GameStateConfig- { initAdjList :: Array Int Text- , initNameList :: Array Int Text+ { initNameParts :: NameGenerator , initEntities :: EntityMap , initRecipes :: [Recipe Entity] , initWorldMap :: WorldMap@@ -1026,50 +1145,135 @@ initGameState gsc = GameState { _creativeMode = False- , _gameStep = WorldTick+ , _temporal =+ TemporalState+ { _gameStep = WorldTick+ , _runStatus = Running+ , _ticks = TickNumber 0+ , _robotStepsPerTick = defaultRobotStepsPerTick+ } , _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 , _robotsWatching = mempty- , _availableRecipes = mempty- , _availableCommands = mempty- , _allDiscoveredEntities = empty+ , _discovery =+ Discovery+ { _availableRecipes = mempty+ , _availableCommands = mempty+ , _allDiscoveredEntities = empty+ , _knownEntities = []+ , -- This does not need to be initialized with anything,+ -- since the master list of achievements is stored in UIState+ _gameAchievements = mempty+ } , _activeRobots = IS.empty , _waitingRobots = M.empty- , _gensym = 0 , _seed = 0 , _randGen = mkStdGen 0- , _adjList = initAdjList gsc- , _nameList = initNameList gsc- , _initiallyRunCode = Nothing- , _entityMap = initEntities gsc- , _recipesOut = outRecipeMap (initRecipes gsc)- , _recipesIn = inRecipeMap (initRecipes gsc)- , _recipesReq = reqRecipeMap (initRecipes gsc)+ , _robotNaming =+ RobotNaming+ { _nameGenerator = initNameParts gsc+ , _gensym = 0+ }+ , _recipesInfo =+ Recipes+ { _recipesOut = outRecipeMap (initRecipes gsc)+ , _recipesIn = inRecipeMap (initRecipes gsc)+ , _recipesCat = catRecipeMap (initRecipes gsc)+ } , _currentScenarioPath = Nothing- , _knownEntities = []- , _worldNavigation = Navigation mempty mempty- , _multiWorld = mempty- , _worldScrollable = True+ , _landscape =+ Landscape+ { _worldNavigation = Navigation mempty mempty+ , _multiWorld = mempty+ , _entityMap = initEntities gsc+ , _worldScrollable = True+ } , _viewCenterRule = VCRobot 0 , _viewCenter = defaultCosmicLocation , _needsRedraw = False- , _replStatus = REPLDone Nothing- , _replNextValueIndex = 0- , _inputHandler = Nothing- , _messageQueue = Empty- , _lastSeenMessageTime = TickNumber (-1)+ , _gameControls =+ GameControls+ { _replStatus = REPLDone Nothing+ , _replNextValueIndex = 0+ , _inputHandler = Nothing+ , _initiallyRunCode = Nothing+ }+ , _messageInfo =+ Messages+ { _messageQueue = Empty+ , _lastSeenMessageTime = TickNumber (-1)+ , _announcementQueue = mempty+ } , _focusedRobotID = 0- , _ticks = TickNumber 0- , _robotStepsPerTick = defaultRobotStepsPerTick } +type SubworldDescription = (SubworldName, ([IndexedTRobot], Seed -> WorldFun Int Entity))++buildWorldTuples :: Scenario -> NonEmpty SubworldDescription+buildWorldTuples s =+ NE.map (worldName &&& buildWorld) $+ s ^. scenarioWorlds++genMultiWorld :: NonEmpty SubworldDescription -> Seed -> W.MultiWorld Int Entity+genMultiWorld worldTuples s =+ M.map genWorld+ . M.fromList+ . NE.toList+ $ worldTuples+ where+ genWorld x = W.newWorld $ snd x s++-- |+-- Returns a list of robots, ordered by decreasing preference+-- to serve as the "base".+--+-- = Rules for selecting the "base" robot:+--+-- What follows is a thorough description of how the base+-- choice is made as of the most recent study of the code.+-- This level of detail is not meant to be public-facing.+--+-- For an abbreviated explanation, see the "Base robot" section of the+-- <https://github.com/swarm-game/swarm/tree/main/data/scenarios#base-robot Scenario Authoring Guide>.+--+-- == Precedence rules+--+-- 1. Prefer those robots defined with a @loc@ ('robotLocation') in the scenario file+--+-- 1. If multiple robots define a @loc@, use the robot that is defined+-- first within the scenario file.+-- 2. Note that if a robot is both given a @loc@ AND is specified in the+-- world map, then two instances of the robot shall be created. The+-- instance with the @loc@ shall be preferred as the base.+--+-- 2. Fall back to robots generated from templates via the map and palette.+--+-- 1. If multiple robots are specified in the map, prefer the one that+-- is defined first within the scenario file.+-- 2. If multiple robots are instantiated from the same template, then+-- prefer the one with a lower-indexed subworld. Note that the root+-- subworld is always first.+-- 3. If multiple robots instantiated from the same template are in the+-- same subworld, then+-- prefer the one closest to the upper-left of the screen, with higher+-- rows given precedence over columns (i.e. first in row-major order).+genRobotTemplates :: Scenario -> NonEmpty (a, ([(Int, TRobot)], b)) -> [TRobot]+genRobotTemplates scenario worldTuples =+ locatedRobots ++ map snd (sortOn fst genRobots)+ where+ -- Keep only robots from the robot list with a concrete location;+ -- the others existed only to serve as a template for robots drawn+ -- in the world map+ locatedRobots = filter (isJust . view trobotLocation) $ scenario ^. scenarioRobots++ -- Subworld order as encountered in the scenario YAML file is preserved for+ -- the purpose of numbering robots, other than the "root" subworld+ -- guaranteed to be first.+ genRobots :: [(Int, TRobot)]+ genRobots = concat $ NE.toList $ NE.map (fst . snd) worldTuples+ -- | Create an initial game state corresponding to the given scenario. scenarioToGameState :: Scenario ->@@ -1088,6 +1292,12 @@ now <- Clock.getTime Clock.Monotonic let robotList' = (robotCreatedAt .~ now) <$> robotList + let modifyRecipesInfo oldRecipesInfo =+ oldRecipesInfo+ & recipesOut %~ addRecipesWith outRecipeMap+ & recipesIn %~ addRecipesWith inRecipeMap+ & recipesCat %~ addRecipesWith catRecipeMap+ return $ (initGameState gsc) { _focusedRobotID = baseID@@ -1098,28 +1308,26 @@ & robotMap .~ IM.fromList (map (view robotID &&& id) robotList') & robotsByLocation .~ M.map (groupRobotsByPlanarLocation . NE.toList) (groupRobotsBySubworld robotList') & internalActiveRobots .~ setOf (traverse . robotID) robotList'- & availableCommands .~ Notifications 0 initialCommands- & gensym .~ initGensym+ & discovery . availableCommands .~ Notifications 0 initialCommands+ & discovery . knownEntities .~ scenario ^. scenarioKnown+ & robotNaming . gensym .~ initGensym & seed .~ theSeed & randGen .~ mkStdGen theSeed- & initiallyRunCode .~ initialCodeToRun- & entityMap .~ em- & recipesOut %~ addRecipesWith outRecipeMap- & recipesIn %~ addRecipesWith inRecipeMap- & recipesReq %~ addRecipesWith reqRecipeMap- & knownEntities .~ scenario ^. scenarioKnown- & worldNavigation .~ scenario ^. scenarioNavigation- & multiWorld .~ allSubworldsMap theSeed+ & recipesInfo %~ modifyRecipesInfo+ & landscape . entityMap .~ em+ & landscape . worldNavigation .~ scenario ^. scenarioNavigation+ & landscape . multiWorld .~ genMultiWorld worldTuples theSeed -- TODO (#1370): Should we allow subworlds to have their own scrollability? -- Leaning toward no , but for now just adopt the root world scrollability -- as being universal.- & worldScrollable .~ NE.head (scenario ^. scenarioWorlds) ^. to scrollable+ & landscape . worldScrollable .~ NE.head (scenario ^. scenarioWorlds) ^. to scrollable & viewCenterRule .~ VCRobot baseID- & replStatus .~ case running of -- When the base starts out running a program, the REPL status must be set to working,+ & gameControls . initiallyRunCode .~ initialCodeToRun+ & gameControls . replStatus .~ case running of -- 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) False -> REPLDone Nothing True -> REPLWorking (Typed Nothing PolyUnit mempty)- & robotStepsPerTick .~ ((scenario ^. scenarioStepsPerTick) ? defaultRobotStepsPerTick)+ & temporal . robotStepsPerTick .~ ((scenario ^. scenarioStepsPerTick) ? defaultRobotStepsPerTick) where groupRobotsBySubworld = binTuples . map (view (robotLocation . subworld) &&& id)@@ -1132,40 +1340,10 @@ em = initEntities gsc <> scenario ^. scenarioEntities baseID = 0 (things, devices) = partition (null . view entityCapabilities) (M.elems (entitiesByName em))- -- Keep only robots from the robot list with a concrete location;- -- the others existed only to serve as a template for robots drawn- -- in the world map- locatedRobots = filter (isJust . view trobotLocation) $ scenario ^. scenarioRobots+ getCodeToRun (CodeToRun _ s) = s - -- Rules for selecting the "base" robot:- -- -------------------------------------- -- What follows is a thorough description of how the base- -- choice is made as of the most recent study of the code.- -- This level of detail is not meant to be public-facing.- --- -- For an abbreviated explanation, see the "Base robot" section of the- -- "Scenario Authoring Guide".- -- https://github.com/swarm-game/swarm/tree/main/data/scenarios#base-robot- --- -- Precedence rules:- -- 1. Prefer those robots defined with a loc in the Scenario file- -- 1.a. If multiple robots define a loc, use the robot that is defined- -- first within the Scenario file.- -- 1.b. Note that if a robot is both given a loc AND is specified in the- -- world map, then two instances of the robot shall be created. The- -- instance with the loc shall be preferred as the base.- -- 2. Fall back to robots generated from templates via the map and palette.- -- 2.a. If multiple robots are specified in the map, prefer the one that- -- is defined first within the Scenario file.- -- 2.b. If multiple robots are instantiated from the same template, then- -- prefer the one with a lower-indexed subworld. Note that the root- -- subworld is always first.- -- 2.c. If multiple robots instantiated from the same template are in the- -- same subworld, then- -- prefer the one closest to the upper-left of the screen, with higher- -- rows given precedence over columns (i.e. first in row-major order).- robotsByBasePrecedence = locatedRobots ++ map snd (sortOn fst genRobots)+ robotsByBasePrecedence = genRobotTemplates scenario worldTuples initialCodeToRun = getCodeToRun <$> toRun @@ -1209,25 +1387,7 @@ (maybe True (`S.member` initialCaps) . constCaps) allConst - -- Subworld order as encountered in the scenario YAML file is preserved for- -- the purpose of numbering robots, other than the "root" subworld- -- guaranteed to be first.- genRobots :: [(Int, TRobot)]- genRobots = concat $ NE.toList $ NE.map (fst . snd) builtWorldTuples-- builtWorldTuples :: NonEmpty (SubworldName, ([IndexedTRobot], Seed -> WorldFun Int Entity))- builtWorldTuples =- NE.map (worldName &&& buildWorld) $- scenario ^. scenarioWorlds-- allSubworldsMap :: Seed -> W.MultiWorld Int Entity- allSubworldsMap s =- M.map genWorld- . M.fromList- . NE.toList- $ builtWorldTuples- where- genWorld x = W.newWorld $ snd x s+ worldTuples = buildWorldTuples scenario theWinCondition = maybe@@ -1244,7 +1404,7 @@ buildWorld WorldDescription {..} = (robots worldName, first fromEnum . wf) where rs = fromIntegral $ length area- cs = fromIntegral $ length (head area)+ cs = fromIntegral $ maybe 0 length $ listToMaybe area Coords (ulr, ulc) = locToCoords ul worldGrid :: [[(TerrainType, Erasable Entity)]]
src/Swarm/Game/Step.hs view
@@ -15,8 +15,8 @@ -- ** Note on the IO: -- -- The only reason we need @IO@ is so that robots can run programs--- loaded from files, via the 'Run' command.--- This could be avoided by using 'Import' command instead and parsing+-- loaded from files, via the 'Swarm.Language.Syntax.Run' command.+-- This could be avoided by using a hypothetical @import@ command instead and parsing -- the required files at the time of declaration. -- See <https://github.com/swarm-game/swarm/issues/495>. module Swarm.Game.Step where@@ -30,8 +30,7 @@ import Control.Effect.Lens import Control.Effect.Lift import Control.Lens as Lens hiding (Const, distrib, from, parts, use, uses, view, (%=), (+=), (.=), (<+=), (<>=))-import Control.Monad (foldM, forM, forM_, guard, join, msum, unless, when, zipWithM)-import Data.Array (bounds, (!))+import Control.Monad (foldM, forM, forM_, guard, msum, unless, when, zipWithM) import Data.Bifunctor (second) import Data.Bool (bool) import Data.Char (chr, ord)@@ -74,8 +73,12 @@ import Swarm.Game.Scenario.Objective qualified as OB import Swarm.Game.Scenario.Objective.WinCheck qualified as WC import Swarm.Game.Scenario.Topography.Navigation.Portal (Navigation (..), destination, reorientation)+import Swarm.Game.Scenario.Topography.Navigation.Util import Swarm.Game.Scenario.Topography.Navigation.Waypoint (WaypointName (..)) import Swarm.Game.State+import Swarm.Game.Step.Combustion qualified as Combustion+import Swarm.Game.Step.Pathfinding+import Swarm.Game.Step.Util import Swarm.Game.Universe import Swarm.Game.Value import Swarm.Game.World qualified as W@@ -91,11 +94,11 @@ import Swarm.Language.Text.Markdown qualified as Markdown import Swarm.Language.Typed (Typed (..)) import Swarm.Language.Value+import Swarm.Log import Swarm.Util hiding (both) import Swarm.Util.Effect (throwToMaybe)+import Swarm.Util.WindowedCounter qualified as WC import System.Clock (TimeSpec)-import System.Clock qualified-import System.Random (UniformRange, uniformR) import Witch (From (from), into) import Prelude hiding (Applicative (..), lookup) @@ -110,10 +113,10 @@ focusedRob <- use focusedRobotID ticked <-- use gameStep >>= \case+ use (temporal . gameStep) >>= \case WorldTick -> do runRobotIDs active- ticks %= addTicks 1+ temporal . ticks %= addTicks 1 pure True RobotStep ss -> singleStep ss focusedRob active @@ -124,11 +127,11 @@ mr <- use (robotMap . at 0) case mr of Just r -> do- res <- use replStatus+ res <- use $ gameControls . replStatus case res of REPLWorking (Typed Nothing ty req) -> case getResult r of Just (v, s) -> do- replStatus .= REPLWorking (Typed (Just v) ty req)+ gameControls . replStatus .= REPLWorking (Typed (Just v) ty req) baseRobot . robotContext . defStore .= s Nothing -> pure () _otherREPLStatus -> pure ()@@ -143,7 +146,7 @@ case wc of WinConditions winState oc -> do g <- get @GameState- em <- use entityMap+ em <- use $ landscape . entityMap hypotheticalWinCheck em g winState oc _ -> pure () return ticked@@ -153,16 +156,16 @@ -- Use this function if you need to unpause the game. finishGameTick :: (Has (State GameState) sig m, Has (Lift IO) sig m) => m () finishGameTick =- use gameStep >>= \case+ use (temporal . gameStep) >>= \case WorldTick -> pure ()- RobotStep SBefore -> gameStep .= WorldTick+ RobotStep SBefore -> temporal . gameStep .= WorldTick RobotStep _ -> void gameTick >> finishGameTick -- Insert the robot back to robot map. -- Will selfdestruct or put the robot to sleep if it has that set. insertBackRobot :: Has (State GameState) sig m => RID -> Robot -> m () insertBackRobot rn rob = do- time <- use ticks+ time <- use $ temporal . ticks if rob ^. selfDestruct then deleteRobot rn else do@@ -192,10 +195,10 @@ -- run robots from the beginning until focused robot SBefore -> do runRobotIDs preFoc- gameStep .= RobotStep (SSingle focRID)+ temporal . gameStep .= RobotStep (SSingle focRID) -- also set ticks of focused robot- steps <- use robotStepsPerTick- robotMap . ix focRID . tickSteps .= steps+ steps <- use $ temporal . robotStepsPerTick+ robotMap . ix focRID . activityCounts . tickStepBudget .= steps -- continue to focused robot if there were no previous robots -- DO NOT SKIP THE ROBOT SETUP above if IS.null preFoc@@ -211,8 +214,8 @@ Nothing | rid == focRID -> do debugLog "The debugged robot does not exist! Exiting single step mode." runRobotIDs postFoc- gameStep .= WorldTick- ticks %= addTicks 1+ temporal . gameStep .= WorldTick+ temporal . ticks %= addTicks 1 return True Nothing | otherwise -> do debugLog "The previously debugged robot does not exist!"@@ -223,7 +226,8 @@ insertBackRobot focRID newR if rid == focRID then do- when (newR ^. tickSteps == 0) $ gameStep .= RobotStep (SAfter focRID)+ when (newR ^. activityCounts . tickStepBudget == 0) $+ temporal . gameStep .= RobotStep (SAfter focRID) return False else do -- continue to newly focused@@ -236,8 +240,8 @@ -- 2. changed focus and the newly focused robot has previously run -- so we just finish the tick the same way runRobotIDs postFoc- gameStep .= RobotStep SBefore- ticks %= addTicks 1+ temporal . gameStep .= RobotStep SBefore+ temporal . ticks %= addTicks 1 return True SAfter rid | otherwise -> do -- go to single step if new robot is focused@@ -246,7 +250,7 @@ where h = hypotheticalRobot (Out VUnit emptyStore []) 0 debugLog txt = do- m <- evalState @Robot h $ createLogEntry (ErrorTrace Debug) txt+ m <- evalState @Robot h $ createLogEntry RobotError Debug txt emitMessage m -- | An accumulator for folding over the incomplete@@ -304,7 +308,7 @@ grantAchievement LoseScenario _ -> return () - announcementQueue %= (>< Seq.fromList (map ObjectiveCompleted $ completionAnnouncementQueue finalAccumulator))+ messageInfo . announcementQueue %= (>< Seq.fromList (map ObjectiveCompleted $ completionAnnouncementQueue finalAccumulator)) mapM_ handleException $ exceptions finalAccumulator where@@ -361,7 +365,7 @@ -- 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+ m <- evalState @Robot h $ createLogEntry RobotError Critical exnText emitMessage m where h = hypotheticalRobot (Out VUnit emptyStore []) 0@@ -372,9 +376,6 @@ m Value evalPT t = evaluateCESK (initMachine t empty emptyStore) -getNow :: Has (Lift IO) sig m => m TimeSpec-getNow = sendIO $ System.Clock.getTime System.Clock.Monotonic- -- | Create a special robot to check some hypothetical, for example the win condition. -- -- Use ID (-1) so it won't conflict with any robots currently in the robot map.@@ -393,6 +394,7 @@ [] True False+ mempty evaluateCESK :: (Has (Lift IO) sig m, Has (Throw Exn) sig m, Has (State GameState) sig m) =>@@ -418,113 +420,31 @@ Nothing -> stepCESK cesk >>= runCESK --------------------------------------------------------------- Some utility functions----------------------------------------------------------------- | Set a flag telling the UI that the world needs to be redrawn.-flagRedraw :: (Has (State GameState) sig m) => m ()-flagRedraw = needsRedraw .= True---- | Perform an action requiring a 'W.World' state component in a--- larger context with a 'GameState'.-zoomWorld ::- (Has (State GameState) sig m) =>- SubworldName ->- StateC (W.World Int Entity) Identity b ->- m (Maybe b)-zoomWorld swName n = do- mw <- use multiWorld- forM (M.lookup swName mw) $ \w -> do- let (w', a) = run (runState w n)- multiWorld %= M.insert swName w'- return a---- | Get the entity (if any) at a given location.-entityAt :: (Has (State GameState) sig m) => Cosmic Location -> m (Maybe Entity)-entityAt (Cosmic subworldName loc) =- join <$> zoomWorld subworldName (W.lookupEntityM @Int (W.locToCoords loc))---- | Modify the entity (if any) at a given location.-updateEntityAt ::- (Has (State GameState) sig m) =>- Cosmic Location ->- (Maybe Entity -> Maybe Entity) ->- m ()-updateEntityAt cLoc@(Cosmic subworldName loc) upd = do- didChange <-- fmap (fromMaybe False) $- zoomWorld subworldName $- W.updateM @Int (W.locToCoords loc) upd- when didChange $- wakeWatchingRobots cLoc---- | Get the robot with a given ID.-robotWithID :: (Has (State GameState) sig m) => RID -> m (Maybe Robot)-robotWithID rid = use (robotMap . at rid)---- | Get the robot with a given name.-robotWithName :: (Has (State GameState) sig m) => Text -> m (Maybe Robot)-robotWithName rname = use (robotMap . to IM.elems . to (find $ \r -> r ^. robotName == rname))---- | Generate a uniformly random number using the random generator in--- the game state.-uniform :: (Has (State GameState) sig m, UniformRange a) => (a, a) -> m a-uniform bnds = do- rand <- use randGen- let (n, g) = uniformR bnds rand- randGen .= g- return n---- | Given a weighting function and a list of values, choose one of--- the values randomly (using the random generator in the game--- state), with the probability of each being proportional to its--- weight. Return @Nothing@ if the list is empty.-weightedChoice :: Has (State GameState) sig m => (a -> Integer) -> [a] -> m (Maybe a)-weightedChoice weight as = do- r <- uniform (0, total - 1)- return $ go r as- where- total = sum (map weight as)-- go _ [] = Nothing- go !k (x : xs)- | k < w = Just x- | otherwise = go (k - w) xs- where- w = weight x---- | Generate a random robot name in the form adjective_name.-randomName :: Has (State GameState) sig m => m Text-randomName = do- adjs <- use @GameState adjList- names <- use @GameState nameList- i <- uniform (bounds adjs)- j <- uniform (bounds names)- return $ T.concat [adjs ! i, "_", names ! j]-------------------------------------------------------------- -- Debugging ------------------------------------------------------------ --- | Create a log entry given current robot and game time in ticks noting whether it has been said.+-- | Create a log entry given current robot and game time in ticks+-- noting whether it has been said. ----- This is the more generic version used both for (recorded) said messages and normal logs.+-- This is the more generic version used both for (recorded) said+-- messages and normal logs. createLogEntry :: (Has (State GameState) sig m, Has (State Robot) sig m) =>- LogSource ->+ RobotLogSource ->+ Severity -> Text -> m LogEntry-createLogEntry source msg = do+createLogEntry source sev msg = do rid <- use robotID rn <- use robotName- time <- use ticks+ time <- use $ temporal . ticks loc <- use robotLocation- pure $ LogEntry time source rn rid (Located loc) msg+ pure $ LogEntry time (RobotLog source rid loc) sev rn msg -- | Print some text via the robot's log.-traceLog :: (Has (State GameState) sig m, Has (State Robot) sig m) => LogSource -> Text -> m LogEntry-traceLog source msg = do- m <- createLogEntry source msg+traceLog :: (Has (State GameState) sig m, Has (State Robot) sig m) => RobotLogSource -> Severity -> Text -> m LogEntry+traceLog source sev msg = do+ m <- createLogEntry source sev msg robotLog %= (Seq.|> m) return m @@ -532,7 +452,7 @@ -- -- Useful for debugging. traceLogShow :: (Has (State GameState) sig m, Has (State Robot) sig m, Show a) => a -> m ()-traceLogShow = void . traceLog Logged . from . show+traceLogShow = void . traceLog Logged Info . from . show ------------------------------------------------------------ -- Exceptions and validation@@ -540,7 +460,7 @@ -- | Capabilities needed for a specific robot to evaluate or execute a -- constant. Right now, the only difference is whether the robot is--- heavy or not when executing the 'Move' command, but there might+-- heavy or not when executing the 'Swarm.Language.Syntax.Move' command, but there might -- be other exceptions added in the future. constCapsFor :: Const -> Robot -> Maybe Capability constCapsFor Move r@@ -565,27 +485,6 @@ (isPrivileged || hasCaps) `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--- system robot, or we are in creative mode).-hasCapability :: (Has (State Robot) sig m, Has (State GameState) sig m) => Capability -> m Bool-hasCapability cap = do- isPrivileged <- isPrivilegedBot- caps <- use robotCapabilities- return (isPrivileged || cap `S.member` caps)---- | Ensure that either a robot has a given capability, OR we are in creative--- mode.-hasCapabilityFor ::- (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 FixByEquip (R.singletonCap cap) term---- | Create an exception about a command failing.-cmdExn :: Const -> [Text] -> Exn-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@@ -613,8 +512,8 @@ -- command execution, whichever comes first. tickRobot :: (Has (State GameState) sig m, Has (Lift IO) sig m) => Robot -> m Robot tickRobot r = do- steps <- use robotStepsPerTick- tickRobotRec (r & tickSteps .~ steps)+ steps <- use $ temporal . robotStepsPerTick+ tickRobotRec (r & activityCounts . tickStepBudget .~ steps) -- | Recursive helper function for 'tickRobot', which checks if the -- robot is actively running and still has steps left, and if so@@ -622,19 +521,31 @@ -- stepping the robot. tickRobotRec :: (Has (State GameState) sig m, Has (Lift IO) sig m) => Robot -> m Robot tickRobotRec r = do- time <- use ticks- case wantsToStep time r && (r ^. runningAtomic || r ^. tickSteps > 0) of+ time <- use $ temporal . ticks+ case wantsToStep time r && (r ^. runningAtomic || r ^. activityCounts . tickStepBudget > 0) of True -> stepRobot r >>= tickRobotRec False -> return r --- | Single-step a robot by decrementing its 'tickSteps' counter and+-- | Single-step a robot by decrementing its 'tickStepBudget' counter and -- running its CESK machine for one step. 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))+ (r', cesk') <- runState (r & activityCounts . tickStepBudget -~ 1) (stepCESK (r ^. machine)) -- sendIO $ appendFile "out.txt" (prettyString cesk' ++ "\n")- return $ r' & machine .~ cesk'+ t <- use $ temporal . ticks + isCreative <- use creativeMode+ let shouldTrackActivity = isCreative || not (r' ^. systemRobot)++ return $+ applyWhen shouldTrackActivity (maintainActivityWindow t) $+ r'+ & machine .~ cesk'+ & activityCounts . lifetimeStepCount +~ 1+ where+ maintainActivityWindow t bot =+ bot & (activityCounts . activityWindow %~ WC.insert t)+ -- | replace some entity in the world with another entity updateWorld :: (Has (State GameState) sig m, Has (Throw Exn) sig m) =>@@ -642,7 +553,7 @@ WorldUpdate Entity -> m () updateWorld c (ReplaceEntity loc eThen down) = do- w <- use multiWorld+ w <- use $ landscape . multiWorld let eNow = W.lookupCosmicEntity (fmap W.locToCoords loc) w -- Can fail if a robot started a multi-tick "drill" operation on some entity -- and meanwhile another entity swaps it out from under them.@@ -701,7 +612,7 @@ -- We wake up robots whose wake-up time has been reached. If it hasn't yet -- then stepCESK is a no-op. Waiting wakeupTime cesk' -> do- time <- use ticks+ time <- use $ temporal . ticks if wakeupTime <= time then stepCESK cesk' else return cesk@@ -824,7 +735,7 @@ return $ Out (VRef loc) s' k -- If we see an update frame, it means we're supposed to set the value -- of a particular cell to the value we just finished computing.- Out v s (FUpdate loc : k) -> return $ Out v (setCell loc (V v) s) k+ Out v s (FUpdate loc : k) -> return $ Out v (setStore loc (V v) s) k ------------------------------------------------------------ -- Execution @@ -832,7 +743,7 @@ -- listing the requirements of the given expression. Out (VRequirements src t _) s (FExec : k) -> do currentContext <- use $ robotContext . defReqs- em <- use entityMap+ em <- use $ landscape . entityMap let (R.Requirements caps devs inv, _) = R.requirements currentContext t devicesForCaps, requiredDevices :: Set (Set Text)@@ -867,7 +778,7 @@ ] ) - _ <- traceLog Logged reqLog+ _ <- traceLog Logged Info reqLog return $ Out VUnit s k -- To execute a definition, we immediately turn the body into a@@ -884,10 +795,10 @@ Out v s (FDef x : k) -> return $ Out (VResult VUnit (singleton x v)) s k -- To execute a constant application, delegate to the 'evalConst'- -- function. Set tickSteps to 0 if the command is supposed to take+ -- function. Set tickStepBudget to 0 if the command is supposed to take -- a tick, so the robot won't take any more steps this tick. Out (VCApp c args) s (FExec : k) -> do- when (isTangible c) $ tickSteps .= 0+ when (isTangible c) $ activityCounts . tickStepBudget .= 0 evalConst c (reverse args) s k -- Reset the runningAtomic flag when we encounter an FFinishAtomic frame.@@ -895,7 +806,7 @@ runningAtomic .= False return $ Out v s k - -- Machinery for implementing the 'meetAll' command.+ -- Machinery for implementing the 'Swarm.Language.Syntax.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@@ -992,10 +903,10 @@ -- cells which were in the middle of being evaluated will be reset. let s' = resetBlackholes s h <- hasCapability CLog- em <- use entityMap+ em <- use $ landscape . entityMap if h then do- void $ traceLog (ErrorTrace Error) (formatExn em exn)+ void $ traceLog RobotError Error (formatExn em exn) return $ Out VUnit s [] else return $ Out VUnit s' [] -- Fatal errors, capability errors, and infinite loop errors can't@@ -1082,7 +993,7 @@ () Nothing "seed"- "A growing seed."+ (Markdown.fromText $ T.unwords ["A growing", e ^. entityName, "seed."]) (Just loc) zero ( defaultEntityDisplay '.'@@ -1094,14 +1005,9 @@ [(1, e)] True False+ mempty ts --- | All functions that are used for robot step can access 'GameState' and the current 'Robot'.------ They can also throw exception of our custom type, which is handled elsewhere.--- Because of that the constraint is only 'Throw', but not 'Catch'/'Error'.-type HasRobotStepState sig m = (Has (State GameState) sig m, Has (State Robot) sig m, Has (Throw Exn) sig m)- -- | Interpret the execution (or evaluation) of a constant application -- to some values. execConst ::@@ -1115,6 +1021,12 @@ -- First, ensure the robot is capable of executing/evaluating this constant. ensureCanExecute c + -- Increment command count regardless of success+ when (isTangible c) $+ activityCounts . tangibleCommandCount += 1++ activityCounts . commandsHistogram %= M.insertWith (+) c 1+ -- Now proceed to actually carry out the operation. case c of Noop -> return $ Out VUnit s k@@ -1123,12 +1035,12 @@ _ -> badConst Wait -> case vs of [VInt d] -> do- time <- use ticks+ time <- use $ temporal . ticks purgeFarAwayWatches- return $ Waiting (addTicks d time) (Out VUnit s k)+ return $ Waiting (addTicks (fromIntegral d) time) (Out VUnit s k) _ -> badConst Selfdestruct -> do- destroyIfNotBase $ Just AttemptSelfDestructBase+ destroyIfNotBase $ \case False -> Just AttemptSelfDestructBase; _ -> Nothing flagRedraw return $ Out VUnit s k Move -> do@@ -1137,6 +1049,29 @@ Backup -> do orient <- use robotOrientation moveInDirection $ applyTurn (DRelative $ DPlanar DBack) $ orient ? zero+ Path -> case vs of+ [VInj hasLimit limitVal, VInj findEntity goalVal] -> do+ maybeLimit <-+ if hasLimit+ then case limitVal of+ VInt d -> return $ Just d+ _ -> badConst+ else return Nothing+ goal <-+ if findEntity+ then case goalVal of+ VText eName -> return $ EntityTarget eName+ _ -> badConst+ else case goalVal of+ VPair (VInt x) (VInt y) ->+ return $+ LocationTarget $+ Location (fromIntegral x) (fromIntegral y)+ _ -> badConst+ robotLoc <- use robotLocation+ result <- pathCommand maybeLimit robotLoc goal+ return $ Out (asValue result) s k+ _ -> badConst Push -> do -- Figure out where we're going loc <- use robotLocation@@ -1196,11 +1131,9 @@ failureMaybes <- mapM checkMoveFailure locsInDirection let maybeFirstFailure = asum failureMaybes - applyMoveFailureEffect maybeFirstFailure $- MoveFailure- { failIfBlocked = ThrowExn- , failIfDrown = Destroy- }+ applyMoveFailureEffect maybeFirstFailure $ \case+ PathBlocked -> ThrowExn+ PathLiquid -> Destroy let maybeLastLoc = do guard $ null maybeFirstFailure@@ -1219,31 +1152,36 @@ nextLoc = fmap (const $ Location (fromIntegral x) (fromIntegral y)) oldLoc onTarget rid $ do- checkMoveAhead nextLoc $- MoveFailure- { failIfBlocked = Destroy- , failIfDrown = Destroy- }+ checkMoveAhead nextLoc $ \case+ PathBlocked -> Destroy+ PathLiquid -> Destroy updateRobotLocation oldLoc nextLoc return $ Out VUnit s k _ -> badConst- Grab -> doGrab Grab'- Harvest -> doGrab Harvest'+ Grab -> mkReturn <$> doGrab Grab'+ Harvest -> mkReturn <$> doGrab Harvest'+ Ignite -> case vs of+ [VDir d] -> do+ Combustion.igniteCommand c d+ return $ Out VUnit s k+ _ -> badConst Swap -> case vs of [VText name] -> do loc <- use robotLocation -- Make sure the robot has the thing in its inventory e <- hasInInventoryOrFail name -- Grab- r <- doGrab Swap'- case r of- Out {} -> do- -- Place the entity and remove it from the inventory- updateEntityAt loc (const (Just e))- robotInventory %= delete e- _ -> pure ()- return r+ newE <- doGrab Swap'++ -- Place the entity and remove it from the inventory+ updateEntityAt loc (const (Just e))+ robotInventory %= delete e++ when (e == newE) $+ grantAchievement SwapSame++ return $ mkReturn newE _ -> badConst Turn -> case vs of [VDir d] -> do@@ -1275,6 +1213,21 @@ flagRedraw return $ Out VUnit s k _ -> badConst+ Ping -> case vs of+ [VRobot otherID] -> do+ maybeOtherRobot <- robotWithID otherID+ selfRobot <- get+ return $ Out (asValue $ displacementVector selfRobot maybeOtherRobot) s k+ where+ displacementVector :: Robot -> Maybe Robot -> Maybe (V2 Int32)+ displacementVector selfRobot maybeOtherRobot = do+ otherRobot <- maybeOtherRobot+ let dist = (cosmoMeasure euclidean `on` view robotLocation) selfRobot otherRobot+ (_minRange, maxRange) = getRadioRange (Just selfRobot) (Just otherRobot)+ d <- getFiniteDistance dist+ guard $ d <= maxRange+ orientationBasedRelativePosition selfRobot $ view robotLocation otherRobot+ _ -> badConst Give -> case vs of [VRobot otherID, VText itemName] -> do -- Make sure the other robot exists and is close@@ -1331,12 +1284,12 @@ [VText name] -> do inv <- use robotInventory ins <- use equippedDevices- em <- use entityMap+ em <- use $ landscape . entityMap e <- lookupEntityName name em `isJustOrFail` ["I've never heard of", indefiniteQ name <> "."] - outRs <- use recipesOut+ outRs <- use $ recipesInfo . recipesOut creative <- use creativeMode let create l = l <> ["You can use 'create \"" <> name <> "\"' instead." | creative]@@ -1439,7 +1392,7 @@ return $ Out (asValue $ loc ^. planar) s k Waypoint -> case vs of [VText name, VInt idx] -> do- lm <- use worldNavigation+ lm <- use $ landscape . worldNavigation Cosmic swName _ <- use robotLocation case M.lookup (WaypointName name) $ M.findWithDefault mempty swName $ waypoints lm of Nothing -> throwError $ CmdFailed Waypoint (T.unwords ["No waypoint named", name]) Nothing@@ -1509,8 +1462,8 @@ -- otherwise have anything reasonable to return. return $ Out (VDir (fromMaybe (DRelative DDown) $ mh >>= toDirection)) s k Time -> do- TickNumber t <- use ticks- return $ Out (VInt t) s k+ TickNumber t <- use $ temporal . ticks+ return $ Out (VInt $ fromIntegral t) s k Drill -> case vs of [VDir d] -> doDrill d _ -> badConst@@ -1613,11 +1566,13 @@ [VText msg] -> do isPrivileged <- isPrivilegedBot loc <- use robotLocation- m <- traceLog Said msg -- current robot will inserted to robot set, so it needs the log++ -- current robot will be inserted into the robot set, so it needs the log+ m <- traceLog Said Info msg emitMessage m- let measureToLog robLoc rawLogLoc = case rawLogLoc of- Located logLoc -> cosmoMeasure manhattan robLoc logLoc- Omnipresent -> Measurable 0+ let measureToLog robLoc = \case+ RobotLog _ _ logLoc -> cosmoMeasure manhattan robLoc logLoc+ SystemLog -> Measurable 0 addLatestClosest rl = \case Seq.Empty -> Seq.singleton m es Seq.:|> e@@ -1626,8 +1581,8 @@ | otherwise -> es |> e where isEarlierThan = (<) `on` (^. leTime)- isFartherThan = (>) `on` (measureToLog rl . view leLocation)- let addToRobotLog :: Has (State GameState) sgn m => Robot -> m ()+ isFartherThan = (>) `on` (measureToLog rl . view leSource)+ let addToRobotLog :: (Has (State GameState) sgn m) => Robot -> m () addToRobotLog r = do maybeRidLoc <- evalState r $ do hasLog <- hasCapability CLog@@ -1651,13 +1606,15 @@ loc <- use robotLocation rid <- use robotID isPrivileged <- isPrivilegedBot- mq <- use messageQueue+ mq <- use $ messageInfo . messageQueue let isClose e = isPrivileged || messageIsFromNearby loc e- let notMine e = rid /= e ^. leRobotID- let limitLast = \case+ notMine e = case e ^. leSource of+ SystemLog {} -> False+ RobotLog _ lrid _ -> rid /= lrid+ limitLast = \case _s Seq.:|> l -> Just $ l ^. leText _ -> Nothing- let mm = limitLast . Seq.filter (liftA2 (&&) notMine isClose) $ Seq.takeWhileR (messageIsRecent gs) mq+ mm = limitLast . Seq.filter (liftA2 (&&) notMine isClose) $ Seq.takeWhileR (messageIsRecent gs) mq return $ maybe (In (TConst Listen) mempty s (FExec : k)) -- continue listening@@ -1665,7 +1622,7 @@ mm Log -> case vs of [VText msg] -> do- void $ traceLog Logged msg+ void $ traceLog Logged Info msg return $ Out VUnit s k _ -> badConst View -> case vs of@@ -1678,7 +1635,7 @@ -- If the robot does not exist... Nothing -> do cr <- use creativeMode- ws <- use worldScrollable+ ws <- use $ landscape . worldScrollable case cr || ws of -- If we are in creative mode or allowed to scroll, then we are allowed -- to learn that the robot doesn't exist.@@ -1714,7 +1671,7 @@ _ -> badConst Create -> case vs of [VText name] -> do- em <- use entityMap+ em <- use $ landscape . entityMap e <- lookupEntityName name em `isJustOrFail` ["I've never heard of", indefiniteQ name <> "."]@@ -1799,7 +1756,7 @@ [VDelay t e] -> return $ In t e s k [VRef loc] -> -- To force a VRef, we look up the location in the store.- case lookupCell loc s of+ case lookupStore loc s of -- If there's no cell at that location, it's a bug! It -- shouldn't be possible to get a VRef to a non-existent -- location, since the only way VRefs get created is at the@@ -1812,7 +1769,7 @@ -- an 'FUpdate' frame so we remember to update the location -- to its value once we finish evaluating it, and focus on -- the expression.- Just (E t e') -> return $ In t e' (setCell loc (Blackhole t e') s) (FUpdate loc : k)+ Just (E t e') -> return $ In t e' (setStore loc (Blackhole t e') s) (FUpdate loc : k) -- If the location contains a Blackhole, that means we are -- already currently in the middle of evaluating it, i.e. it -- depends on itself, so throw an 'InfiniteLoop' error.@@ -1858,7 +1815,7 @@ _ -> badConst InstallKeyHandler -> case vs of [VText hint, handler] -> do- inputHandler .= Just (hint, handler)+ gameControls . inputHandler .= Just (hint, handler) return $ Out VUnit s k _ -> badConst Reprogram -> case vs of@@ -1949,6 +1906,7 @@ -- Pick a random display name. displayName <- randomName createdAt <- getNow+ isSystemRobot <- use systemRobot -- Construct the new robot and add it to the world. parentCtx <- use robotContext@@ -1967,8 +1925,9 @@ (In cmd e s [FExec]) [] []- False+ isSystemRobot False+ mempty createdAt -- Provision the new robot with the necessary devices and inventory.@@ -1996,7 +1955,7 @@ -- Copy over the salvaged robot's log, if we have one inst <- use equippedDevices- em <- use entityMap+ em <- use $ landscape . entityMap isPrivileged <- isPrivilegedBot logger <- lookupEntityName "logger" em@@ -2030,8 +1989,8 @@ activateRobot (target ^. robotID) -- Now wait the right amount of time for it to finish.- time <- use ticks- return $ Waiting (addTicks (fromIntegral numItems + 1) time) (Out VUnit s k)+ time <- use $ temporal . ticks+ return $ Waiting (addTicks (numItems + 1) time) (Out VUnit s k) _ -> badConst -- run can take both types of text inputs -- with and without file extension as in@@ -2127,10 +2086,10 @@ applyDevice ins verbPhrase d tool = do (nextLoc, nextE) <- getDeviceTarget verbPhrase d- inRs <- use recipesIn+ inRs <- use $ recipesInfo . recipesIn let recipes = filter isApplicableRecipe (recipesFor inRs nextE)- isApplicableRecipe = any ((== tool) . snd) . view recipeRequirements+ isApplicableRecipe = any ((== tool) . snd) . view recipeCatalysts not (null recipes) `holdsOrFail` [ "There is no way to"@@ -2273,25 +2232,12 @@ updateWorldAndRobots c wf rf return $ Out v s k else do- time <- use ticks- return . (if remTime <= 1 then id else Waiting (addTicks remTime time)) $+ time <- use $ temporal . ticks+ return . (if remTime <= 1 then id else Waiting (addTicks (fromIntegral remTime) time)) $ Out v s (FImmediate c wf rf : k) where remTime = r ^. recipeTime - deriveHeading :: HasRobotStepState sig m => Direction -> m Heading- deriveHeading d = do- orient <- use robotOrientation- when (isCardinal d) $ hasCapabilityFor COrient $ TDir d- return $ applyTurn d $ orient ? zero-- lookInDirection :: HasRobotStepState sig m => Direction -> m (Cosmic Location, Maybe Entity)- lookInDirection d = do- newHeading <- deriveHeading d- loc <- use robotLocation- let nextLoc = loc `offsetBy` newHeading- (nextLoc,) <$> entityAt nextLoc- ensureEquipped :: HasRobotStepState sig m => Text -> m Entity ensureEquipped itemName = do inst <- use equippedDevices@@ -2342,7 +2288,7 @@ m (Set Entity, Inventory) checkRequirements parentInventory childInventory childDevices cmd subject fixI = do currentContext <- use $ robotContext . defReqs- em <- use entityMap+ em <- use $ landscape . entityMap creative <- use creativeMode let -- Note that _capCtx must be empty: at least at the -- moment, definitions are only allowed at the top level,@@ -2456,76 +2402,67 @@ return (minimalEquipSet, missingChildInv) - destroyIfNotBase :: HasRobotStepState sig m => Maybe GameplayAchievement -> m ()+ -- Destroy the current robot, as long as it is not the base robot.+ --+ -- Depending on whether we destroy (True) or do not destroy+ -- (False) the current robot, possibly grant an achievement.+ --+ -- Note we cannot simply return a Boolean and grant achievements+ -- at call sites, because in the case that we do not destroy the+ -- base we actually throw an exception, so we do not return to the+ -- original call site.+ destroyIfNotBase ::+ (HasRobotStepState sig m, Has (Lift IO) sig m) =>+ (Bool -> Maybe GameplayAchievement) ->+ m () destroyIfNotBase mAch = do rid <- use robotID holdsOrFailWithAchievement (rid /= 0) ["You consider destroying your base, but decide not to do it after all."]- mAch+ (mAch False)+ selfDestruct .= True+ maybe (return ()) grantAchievement (mAch True) moveInDirection :: (HasRobotStepState sig m, Has (Lift IO) sig m) => Heading -> m CESK moveInDirection orientation = do -- Figure out where we're going loc <- use robotLocation let nextLoc = loc `offsetBy` orientation- checkMoveAhead nextLoc $- MoveFailure- { failIfBlocked = ThrowExn- , failIfDrown = Destroy- }+ checkMoveAhead nextLoc $ \case+ PathBlocked -> ThrowExn+ PathLiquid -> Destroy updateRobotLocation loc nextLoc return $ Out VUnit s k - -- Make sure nothing is in the way. Note that system robots implicitly ignore- -- and base throws on failure.- checkMoveFailure :: HasRobotStepState sig m => Cosmic Location -> m (Maybe MoveFailureDetails)- checkMoveFailure nextLoc = do- me <- entityAt nextLoc- systemRob <- use systemRobot- caps <- use robotCapabilities- return $ do- e <- me- guard $ not systemRob- go caps e- where- go caps e- -- robots can not walk through walls- | e `hasProperty` Unwalkable = Just $ MoveFailureDetails e PathBlocked- -- robots drown if they walk over liquid without boat- | e `hasProperty` Liquid && CFloat `S.notMember` caps =- Just $ MoveFailureDetails e PathLiquid- | otherwise = Nothing- applyMoveFailureEffect ::- HasRobotStepState sig m =>+ (HasRobotStepState sig m, Has (Lift IO) sig m) => Maybe MoveFailureDetails ->- MoveFailure ->+ MoveFailureHandler -> m ()- applyMoveFailureEffect maybeFailure MoveFailure {..} =+ applyMoveFailureEffect maybeFailure failureHandler = case maybeFailure of Nothing -> return ()- Just (MoveFailureDetails e failureMode) -> case failureMode of- PathBlocked ->- handleFailure- failIfBlocked- ["There is a", e ^. entityName, "in the way!"]- PathLiquid ->- handleFailure- failIfDrown- ["There is a dangerous liquid", e ^. entityName, "in the way!"]- where- handleFailure behavior message = case behavior of- Destroy -> destroyIfNotBase Nothing- ThrowExn -> throwError $ cmdExn c message- IgnoreFail -> return ()+ Just (MoveFailureDetails e failureMode) -> case failureHandler failureMode of+ IgnoreFail -> return ()+ Destroy -> destroyIfNotBase $ \b -> case (b, failureMode) of+ (True, PathLiquid) -> Just RobotIntoWater -- achievement for drowning+ _ -> Nothing+ ThrowExn -> throwError . cmdExn c $+ case failureMode of+ PathBlocked -> ["There is a", e ^. entityName, "in the way!"]+ PathLiquid -> ["There is a dangerous liquid", e ^. entityName, "in the way!"] -- Determine the move failure mode and apply the corresponding effect.- checkMoveAhead :: HasRobotStepState sig m => Cosmic Location -> MoveFailure -> m ()- checkMoveAhead nextLoc failureHandlers = do+ checkMoveAhead ::+ (HasRobotStepState sig m, Has (Lift IO) sig m) =>+ Cosmic Location ->+ MoveFailureHandler ->+ m ()+ checkMoveAhead nextLoc failureHandler = do maybeFailure <- checkMoveFailure nextLoc- applyMoveFailureEffect maybeFailure failureHandlers+ applyMoveFailureEffect maybeFailure failureHandler getRobotWithinTouch :: HasRobotStepState sig m => RID -> m Robot getRobotWithinTouch rid = do@@ -2546,7 +2483,7 @@ return other holdsOrFail :: (Has (Throw Exn) sig m) => Bool -> [Text] -> m ()- holdsOrFail a ts = a `holdsOr` cmdExn c ts+ holdsOrFail = holdsOrFail' c holdsOrFailWithAchievement :: (Has (Throw Exn) sig m) => Bool -> [Text] -> Maybe GameplayAchievement -> m () holdsOrFailWithAchievement a ts mAch = case mAch of@@ -2554,7 +2491,7 @@ 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+ isJustOrFail = isJustOrFail' c returnEvalCmp = case vs of [v1, v2] -> (\b -> Out (VBool b) s k) <$> evalCmp c v1 v2@@ -2575,9 +2512,12 @@ `holdsOrFail` ["You don't have", indefinite eName, "to", cmd <> "."] return e + mkReturn :: Valuable a => a -> CESK+ mkReturn x = Out (asValue x) s k+ -- The code for grab and harvest is almost identical, hence factored -- out here.- doGrab :: (HasRobotStepState sig m, Has (Lift IO) sig m) => GrabbingCmd -> m CESK+ doGrab :: (HasRobotStepState sig m, Has (Lift IO) sig m) => GrabbingCmd -> m Entity doGrab cmd = do let verb = verbGrabbingCmd cmd verbed = verbedGrabbingCmd cmd@@ -2616,13 +2556,13 @@ let yieldName = e ^. entityYields e' <- case yieldName of Nothing -> return e- Just n -> fromMaybe e <$> uses entityMap (lookupEntityName n)+ Just n -> fromMaybe e <$> uses (landscape . entityMap) (lookupEntityName n) robotInventory %= insert e' updateDiscoveredEntities e' - -- Return the name of the item obtained.- return $ Out (VText (e' ^. entityName)) s k+ -- Return the item obtained.+ return e' ------------------------------------------------------------ -- The "watch" command@@ -2656,11 +2596,6 @@ -- Some utility functions ------------------------------------------------------------ --- | Exempts the robot from various command constraints--- when it is either a system robot or playing in creative mode-isPrivilegedBot :: (Has (State GameState) sig m, Has (State Robot) sig m) => m Bool-isPrivilegedBot = (||) <$> use systemRobot <*> use creativeMode- -- | Requires that the target location is within one cell. -- Requirement is waived if the bot is privileged. isNearbyOrExempt :: Bool -> Cosmic Location -> Cosmic Location -> Bool@@ -2676,23 +2611,18 @@ grantAchievement a = do currentTime <- sendIO getZonedTime scenarioPath <- use currentScenarioPath- gameAchievements+ discovery . gameAchievements %= M.insertWith (<>) a (Attainment (GameplayAchievement a) scenarioPath currentTime) -data MoveFailureMode = PathBlocked | PathLiquid-data MoveFailureDetails = MoveFailureDetails Entity MoveFailureMode- -- | How to handle failure, for example when moving to blocked location data RobotFailure = ThrowExn | Destroy | IgnoreFail --- | How to handle failure when moving/teleporting to a location.-data MoveFailure = MoveFailure- { failIfBlocked :: RobotFailure- , failIfDrown :: RobotFailure- }+-- | How to handle different types of failure when moving/teleporting+-- to a location.+type MoveFailureHandler = MoveFailureMode -> RobotFailure data GrabbingCmd = Grab' | Harvest' | Swap' | Push' deriving (Eq, Show) @@ -2717,7 +2647,8 @@ -- | 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.+-- by the given 'RID') as part of a 'Swarm.Language.Syntax.Build'+-- or 'Swarm.Language.Syntax.Reprogram' command. -- The first 'Inventory' is devices to be equipped, and the second -- is entities to be transferred. --@@ -2762,7 +2693,7 @@ flagRedraw where applyPortal loc = do- lms <- use worldNavigation+ lms <- use $ landscape . worldNavigation let maybePortalInfo = M.lookup loc $ portals lms updatedLoc = maybe loc destination maybePortalInfo maybeTurn = reorientation <$> maybePortalInfo@@ -2773,9 +2704,9 @@ -- | Execute a stateful action on a target robot --- whether the -- current one or another. onTarget ::- HasRobotStepState sig m =>+ (HasRobotStepState sig m, Has (Lift IO) sig m) => RID ->- (forall sig' m'. (HasRobotStepState sig' m') => m' ()) ->+ (forall sig' m'. (HasRobotStepState sig' m', Has (Lift IO) sig' m') => m' ()) -> m () onTarget rid act = do myID <- use robotID@@ -2898,14 +2829,14 @@ -- | Update the global list of discovered entities, and check for new recipes. updateDiscoveredEntities :: (HasRobotStepState sig m) => Entity -> m () updateDiscoveredEntities e = do- allDiscovered <- use allDiscoveredEntities+ allDiscovered <- use $ discovery . allDiscoveredEntities if E.contains0plus e allDiscovered then pure () else do let newAllDiscovered = E.insertCount 1 e allDiscovered updateAvailableRecipes (newAllDiscovered, newAllDiscovered) e updateAvailableCommands e- allDiscoveredEntities .= newAllDiscovered+ discovery . allDiscoveredEntities .= newAllDiscovered -- | Update the availableRecipes list. -- This implementation is not efficient:@@ -2917,13 +2848,13 @@ -- But it probably doesn't really make that much difference until we get up to thousands of recipes. updateAvailableRecipes :: Has (State GameState) sig m => (Inventory, Inventory) -> Entity -> m () updateAvailableRecipes invs e = do- allInRecipes <- use recipesIn+ allInRecipes <- use $ recipesInfo . recipesIn let entityRecipes = recipesFor allInRecipes e usableRecipes = filter (knowsIngredientsFor invs) entityRecipes- knownRecipes <- use (availableRecipes . notificationsContent)+ knownRecipes <- use $ discovery . availableRecipes . notificationsContent let newRecipes = filter (`notElem` knownRecipes) usableRecipes newCount = length newRecipes- availableRecipes %= mappend (Notifications newCount newRecipes)+ discovery . availableRecipes %= mappend (Notifications newCount newRecipes) updateAvailableCommands e updateAvailableCommands :: Has (State GameState) sig m => Entity -> m ()@@ -2933,7 +2864,7 @@ Just cap -> cap `S.member` newCaps Nothing -> False entityConsts = filter (keepConsts . constCaps) allConst- knownCommands <- use (availableCommands . notificationsContent)+ knownCommands <- use $ discovery . availableCommands . notificationsContent let newCommands = filter (`notElem` knownCommands) entityConsts newCount = length newCommands- availableCommands %= mappend (Notifications newCount newCommands)+ discovery . availableCommands %= mappend (Notifications newCount newCommands)
+ src/Swarm/Game/Step/Combustion.hs view
@@ -0,0 +1,227 @@+{-# LANGUAGE BlockArguments #-}+{-# LANGUAGE ConstraintKinds #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}++-- |+-- SPDX-License-Identifier: BSD-3-Clause+--+-- Some entities are "combustible". A command, 'Swarm.Language.Syntax.Ignite', will+-- initiate combustion on such an entity.+-- Furthermore, combustion can spread to (4-)adjacent entities, depending+-- on the 'ignition' property of that entity.+--+-- Short-lived robots are used to illustrate the combusting entity as+-- well as to initiate the delayed combustion of its neighbors.+module Swarm.Game.Step.Combustion where++import Control.Applicative (Applicative (..))+import Control.Carrier.State.Lazy+import Control.Effect.Lens+import Control.Effect.Lift+import Control.Lens as Lens hiding (Const, distrib, from, parts, use, uses, view, (%=), (+=), (.=), (<+=), (<>=))+import Control.Monad (forM_, void, when)+import Data.Text qualified as T+import Linear (zero)+import Swarm.Game.CESK (emptyStore, initMachine)+import Swarm.Game.Display+import Swarm.Game.Entity hiding (empty, lookup, singleton, union)+import Swarm.Game.Entity qualified as E+import Swarm.Game.Location+import Swarm.Game.Robot+import Swarm.Game.State+import Swarm.Game.Step.Util+import Swarm.Game.Universe+import Swarm.Language.Context (empty)+import Swarm.Language.Pipeline (ProcessedTerm)+import Swarm.Language.Pipeline.QQ (tmQ)+import Swarm.Language.Syntax+import Swarm.Language.Text.Markdown qualified as Markdown+import Swarm.Util hiding (both)+import System.Clock (TimeSpec)+import Prelude hiding (Applicative (..), lookup)++igniteCommand :: (HasRobotStepState sig m, Has (Lift IO) sig m) => Const -> Direction -> m ()+igniteCommand c d = do+ (loc, me) <- lookInDirection d+ -- Ensure there is an entity here.+ e <-+ me `isJustOrFail` ["There is nothing here to", verb <> "."]++ -- Ensure it can be ignited.+ (e `hasProperty` Combustible)+ `holdsOrFail` ["The", e ^. entityName, "here can't be", verbed <> "."]++ -- Remove the entity from the world.+ updateEntityAt loc (const Nothing)+ flagRedraw++ -- Start burning process+ let selfCombustibility = (e ^. entityCombustion) ? defaultCombustibility+ createdAt <- getNow+ combustionDurationRand <- addCombustionBot e selfCombustibility createdAt loc+ forM_ (getNeighborLocs loc) $ igniteNeighbor createdAt combustionDurationRand+ where+ verb = "ignite"+ verbed = "ignited"++ holdsOrFail = holdsOrFail' c+ isJustOrFail = isJustOrFail' c++-- | Construct a "combustion robot" from entity and position+-- and add it to the world.+-- It has low priority and will be covered+-- by placed entities.+-- The "combustion bot" represents the burning of a single+-- entity; propagating the fire to neighbors is handled upstream,+-- within the 'Swarm.Language.Syntax.Ignite' command.+addCombustionBot ::+ Has (State GameState) sig m =>+ Entity ->+ Combustibility ->+ TimeSpec ->+ Cosmic Location ->+ m Integer+addCombustionBot inputEntity combustibility ts loc = do+ botInventory <- case maybeCombustionProduct of+ Nothing -> return []+ Just n -> do+ maybeE <- uses (landscape . entityMap) (lookupEntityName n)+ return $ maybe [] (pure . (1,)) maybeE+ combustionDurationRand <- uniform durationRange+ let combustionProg = combustionProgram combustionDurationRand combustibility+ void $+ addTRobot $+ mkRobot+ ()+ Nothing+ "fire"+ (Markdown.fromText $ T.unwords ["A burning", (inputEntity ^. entityName) <> "."])+ (Just loc)+ zero+ ( defaultEntityDisplay '*'+ & displayAttr .~ AWorld "fire"+ & displayPriority .~ 0+ )+ (initMachine combustionProg empty emptyStore)+ []+ botInventory+ True+ False+ mempty+ ts+ return combustionDurationRand+ where+ Combustibility _ durationRange maybeCombustionProduct = combustibility++-- Triggers the ignition of the entity underfoot with some delay.+ignitionProgram :: Integer -> ProcessedTerm+ignitionProgram waitTime =+ [tmQ|+ wait $int:waitTime;+ try {+ ignite down;+ noop;+ } {};+ selfdestruct+ |]++-- | A system program for a "combustion robot", to burn an entity+-- after it is ignited.+--+-- For efficiency, we determine a priori (i.e. the instant+-- the combustion robot is spawned) whether any neighbors will eventually+-- be burned, based on probabilities.+--+-- Note that it is possible that new neighbors may be introduced while+-- combustion is in progress. Although it may be more realistic to subject+-- these to possible combustion as well, we do not bother.+--+-- Though if we did actually want to do that, some options are:+--+-- 1. Create sub-partitions (of say, 10-tick duration) of the combustion duration+-- to re-evaluate opportunities to light adjacent entities on fire.+-- 2. Use the 'Swarm.Language.Syntax.Watch' command to observe for changes to adjacent entities.+-- Note that if we "wake" from our 'Swarm.Language.Syntax.Wait' due to the 'Swarm.Language.Syntax.Watch' being triggered,+-- we would need to maintain bookkeeping of how much time is left.+-- 3. Spawn more robots whose sole purpose is to observe for changes to neighbor+-- cells. This would avoid polluting the logic of the currently burning cell+-- with logic to manage probabilities of combustion propagation.+combustionProgram :: Integer -> Combustibility -> ProcessedTerm+combustionProgram combustionDuration (Combustibility _ _ maybeCombustionProduct) =+ [tmQ|+ wait $int:combustionDuration;+ if ($int:invQuantity > 0) {+ try {+ place $str:combustionProduct;+ } {};+ } {};+ selfdestruct+ |]+ where+ (invQuantity, combustionProduct) = case maybeCombustionProduct of+ Nothing -> (0, "")+ Just p -> (1, p)++-- | We treat the 'ignition' field in the 'Combustibility' record+-- as a /rate/ in a Poisson distribution.+-- Ignition of neighbors depends on that particular neighbor entity's+-- combustion /rate/, but also on the duration+-- that the current entity will burn.+igniteNeighbor ::+ Has (State GameState) sig m =>+ TimeSpec ->+ Integer ->+ Cosmic Location ->+ m ()+igniteNeighbor creationTime sourceDuration loc = do+ maybeEnt <- entityAt loc+ forM_ maybeEnt igniteEntity+ where+ igniteEntity e =+ when (e `hasProperty` Combustible) $ do+ threshold <- uniform (0, 1)+ when (probabilityOfIgnition >= threshold) $ do+ ignitionDelayRand <- uniform (0, 1)+ let ignitionDelay =+ floor+ . min (fromIntegral sourceDuration)+ . negate+ $ log ignitionDelayRand / rate+ addIgnitionBot ignitionDelay e creationTime loc+ where+ neighborCombustibility = (e ^. entityCombustion) ? defaultCombustibility+ rate = E.ignition neighborCombustibility+ probabilityOfIgnition = 1 - exp (negate $ rate * fromIntegral sourceDuration)++-- | Construct an invisible "ignition robot" and add it to the world.+-- Its sole purpose is to delay the 'Swarm.Language.Syntax.Ignite' command for a neighbor+-- that has been a priori determined that it shall be ignited.+addIgnitionBot ::+ Has (State GameState) sig m =>+ Integer ->+ Entity ->+ TimeSpec ->+ Cosmic Location ->+ m ()+addIgnitionBot ignitionDelay inputEntity ts loc =+ void $+ addTRobot $+ mkRobot+ ()+ Nothing+ "firestarter"+ (Markdown.fromText $ T.unwords ["Delayed ignition of", (inputEntity ^. entityName) <> "."])+ (Just loc)+ zero+ ( defaultEntityDisplay '*'+ & invisible .~ True+ )+ (initMachine (ignitionProgram ignitionDelay) empty emptyStore)+ []+ []+ True+ False+ mempty+ ts
+ src/Swarm/Game/Step/Pathfinding.hs view
@@ -0,0 +1,137 @@+-- |+-- SPDX-License-Identifier: BSD-3-Clause+--+-- Implementation of the @path@ command for robots.+--+-- = Design considerations+-- One possible design of the @path@ command entailed storing a computed+-- shortest path and providing a mechanism to retrieve parts of it later+-- without recomputing the whole thing.+-- However, in general the playfield can be dynamic and obstructions may+-- appear that invalidate a given computed shortest path.+-- Therefore, there can be limited value in caching a computed path for use+-- across ticks.+--+-- Instead, in the current implementation a complete path is computed+-- internally upon invoking the @path@ command, and just the direction of the+-- first "move" along that path is returned as a result to the caller.+--+-- == Max distance+--+-- We allow the caller to supply a max distance, but also impose an internal maximum+-- distance to prevent programming errors from irrecoverably freezing the game.+module Swarm.Game.Step.Pathfinding where++import Control.Carrier.State.Lazy+import Control.Effect.Lens+import Control.Monad (filterM, guard)+import Control.Monad.Trans.Class (lift)+import Control.Monad.Trans.Maybe (MaybeT (..), runMaybeT)+import Data.Graph.AStar (aStarM)+import Data.HashSet (HashSet)+import Data.HashSet qualified as HashSet+import Data.Int (Int32)+import Swarm.Game.Entity+import Swarm.Game.Location+import Swarm.Game.State+import Swarm.Game.Step.Util+import Swarm.Game.Universe+import Swarm.Language.Syntax+import Swarm.Util (hoistMaybe)++-- | Shortest paths can either be computed to the nearest entity of+-- a given type or to a specific location.+data PathfindingTarget+ = LocationTarget Location+ | -- | Note: navigation to entities does not benefit from the+ -- distance heuristic optimization of the A* algorithm.+ EntityTarget EntityName++-- | swarm command arguments are converted to idiomatic Haskell+-- types before invoking this function, and conversely the callsite+-- is also responsible for translating the output type to a swarm value.+--+-- The cost function is uniformly @1@ between adjacent cells.+--+-- Viable paths are determined by walkability.+-- If the goal type is an Entity, than it is permissible for that+-- entity to be 'Unwalkable'.+pathCommand ::+ (HasRobotStepState sig m, Has (State GameState) sig m) =>+ -- | Distance limit+ Maybe Integer ->+ -- | Starting location+ Cosmic Location ->+ -- | Search goal+ PathfindingTarget ->+ m (Maybe Direction)+pathCommand maybeLimit (Cosmic currentSubworld robotLoc) target = do+ -- This is a short-circuiting optimization; if the goal itself+ -- is not a walkable cell, then no amount of searching will reach it.+ isGoalLocWalkable <- case target of+ LocationTarget loc -> null <$> checkMoveFailure (Cosmic currentSubworld loc)+ EntityTarget _ -> return True++ runMaybeT $ do+ guard isGoalLocWalkable+ maybeFoundPath <- lift computePath+ foundPath <- hoistMaybe maybeFoundPath+ return $ nextDir foundPath+ where+ computePath =+ aStarM+ (neighborFunc withinDistanceLimit . Cosmic currentSubworld)+ (const $ const $ return 1)+ (return . distHeuristic)+ goalReachedFunc+ (return robotLoc)++ withinDistanceLimit :: Location -> Bool+ withinDistanceLimit = (<= distanceLimit) . fromIntegral . manhattan robotLoc++ -- Extracts the head of the found path to determine+ -- the next direction for the robot to proceed along+ nextDir :: [Location] -> Direction+ nextDir pathLocs = case pathLocs of+ [] -> DRelative DDown+ (nextLoc : _) -> DAbsolute $ nearestDirection $ nextLoc .-. robotLoc++ neighborFunc ::+ HasRobotStepState sig m =>+ (Location -> Bool) ->+ Cosmic Location ->+ m (HashSet Location)+ neighborFunc isWithinRange loc = do+ locs <- filterM isWalkableLoc neighborLocs+ return $ HashSet.fromList $ map (view planar) locs+ where+ neighborLocs = getNeighborLocs loc+ isWalkableLoc someLoc =+ if not $ isWithinRange $ view planar someLoc+ then return False+ else do+ isGoal <- goalReachedFunc $ view planar someLoc+ if isGoal+ then return True+ else null <$> checkMoveFailureUnprivileged someLoc++ -- This is an optimization for when a specific location+ -- is given as the target.+ -- However, it is not strictly necessary, and in fact+ -- cannot be used when the target is a certain type of+ -- entity.+ distHeuristic :: Location -> Int32+ distHeuristic = case target of+ LocationTarget gLoc -> manhattan gLoc+ EntityTarget _eName -> const 0++ goalReachedFunc :: Has (State GameState) sig m => Location -> m Bool+ goalReachedFunc loc = case target of+ LocationTarget gLoc -> return $ loc == gLoc+ EntityTarget eName -> do+ me <- entityAt $ Cosmic currentSubworld loc+ return $ (view entityName <$> me) == Just eName++ -- A failsafe limit is hardcoded to prevent the game from freezing+ -- if an error exists in some .sw code.+ distanceLimit = maybe maxPathRange (min maxPathRange) maybeLimit
+ src/Swarm/Game/Step/Util.hs view
@@ -0,0 +1,224 @@+{-# LANGUAGE BlockArguments #-}+{-# LANGUAGE ConstraintKinds #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE OverloadedStrings #-}++-- |+-- SPDX-License-Identifier: BSD-3-Clause+module Swarm.Game.Step.Util where++import Control.Applicative (Applicative (..))+import Control.Carrier.State.Lazy+import Control.Effect.Error+import Control.Effect.Lens+import Control.Effect.Lift+import Control.Lens as Lens hiding (Const, distrib, from, parts, use, uses, view, (%=), (+=), (.=), (<+=), (<>=))+import Control.Monad (forM, forM_, guard, join, when)+import Control.Monad.Trans.Class (lift)+import Control.Monad.Trans.Maybe (MaybeT (..), runMaybeT)+import Data.Array (bounds, (!))+import Data.IntMap qualified as IM+import Data.List (find)+import Data.Map qualified as M+import Data.Set qualified as S+import Data.Text (Text)+import Data.Text qualified as T+import Linear (zero)+import Swarm.Game.Entity hiding (empty, lookup, singleton, union)+import Swarm.Game.Exception+import Swarm.Game.Location+import Swarm.Game.ResourceLoading (NameGenerator (..))+import Swarm.Game.Robot+import Swarm.Game.State+import Swarm.Game.Universe+import Swarm.Game.World qualified as W+import Swarm.Game.World.Modify qualified as WM+import Swarm.Language.Capability+import Swarm.Language.Requirement qualified as R+import Swarm.Language.Syntax+import Swarm.Util hiding (both)+import System.Clock (TimeSpec)+import System.Clock qualified+import System.Random (UniformRange, uniformR)+import Prelude hiding (Applicative (..), lookup)++-- | All functions that are used for robot step can access 'GameState' and the current 'Robot'.+--+-- They can also throw exception of our custom type, which is handled elsewhere.+-- Because of that the constraint is only 'Throw', but not 'Catch'/'Error'.+type HasRobotStepState sig m = (Has (State GameState) sig m, Has (State Robot) sig m, Has (Throw Exn) sig m)++deriveHeading :: HasRobotStepState sig m => Direction -> m Heading+deriveHeading d = do+ orient <- use robotOrientation+ when (isCardinal d) $ hasCapabilityFor COrient $ TDir d+ return $ applyTurn d $ orient ? zero++lookInDirection :: HasRobotStepState sig m => Direction -> m (Cosmic Location, Maybe Entity)+lookInDirection d = do+ newHeading <- deriveHeading d+ loc <- use robotLocation+ let nextLoc = loc `offsetBy` newHeading+ (nextLoc,) <$> entityAt nextLoc++-- | Modify the entity (if any) at a given location.+updateEntityAt ::+ (Has (State GameState) sig m) =>+ Cosmic Location ->+ (Maybe Entity -> Maybe Entity) ->+ m ()+updateEntityAt cLoc@(Cosmic subworldName loc) upd = do+ someChange <-+ zoomWorld subworldName $+ W.updateM @Int (W.locToCoords loc) upd++ forM_ (WM.getModification =<< someChange) $ \_modType -> do+ wakeWatchingRobots cLoc++-- * Capabilities++-- | Exempts the robot from various command constraints+-- when it is either a system robot or playing in creative mode+isPrivilegedBot :: (Has (State GameState) sig m, Has (State Robot) sig m) => m Bool+isPrivilegedBot = (||) <$> use systemRobot <*> use creativeMode++-- | Test whether the current robot has a given capability (either+-- because it has a device which gives it that capability, or it is a+-- system robot, or we are in creative mode).+hasCapability :: (Has (State Robot) sig m, Has (State GameState) sig m) => Capability -> m Bool+hasCapability cap = do+ isPrivileged <- isPrivilegedBot+ caps <- use robotCapabilities+ return (isPrivileged || cap `S.member` caps)++-- | Ensure that either a robot has a given capability, OR we are in creative+-- mode.+hasCapabilityFor ::+ (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 FixByEquip (R.singletonCap cap) term++-- * Exceptions++holdsOrFail' :: (Has (Throw Exn) sig m) => Const -> Bool -> [Text] -> m ()+holdsOrFail' c a ts = a `holdsOr` cmdExn c ts++isJustOrFail' :: (Has (Throw Exn) sig m) => Const -> Maybe a -> [Text] -> m a+isJustOrFail' c a ts = a `isJustOr` cmdExn c ts++-- | Create an exception about a command failing.+cmdExn :: Const -> [Text] -> Exn+cmdExn c parts = CmdFailed c (T.unwords parts) Nothing++-- * Some utility functions++getNow :: Has (Lift IO) sig m => m TimeSpec+getNow = sendIO $ System.Clock.getTime System.Clock.Monotonic++-- | Set a flag telling the UI that the world needs to be redrawn.+flagRedraw :: (Has (State GameState) sig m) => m ()+flagRedraw = needsRedraw .= True++-- * World queries++getNeighborLocs :: Cosmic Location -> [Cosmic Location]+getNeighborLocs loc = map (offsetBy loc . flip applyTurn north . DRelative . DPlanar) listEnums++-- | Perform an action requiring a 'W.World' state component in a+-- larger context with a 'GameState'.+zoomWorld ::+ (Has (State GameState) sig m) =>+ SubworldName ->+ StateC (W.World Int Entity) Identity b ->+ m (Maybe b)+zoomWorld swName n = do+ mw <- use $ landscape . multiWorld+ forM (M.lookup swName mw) $ \w -> do+ let (w', a) = run (runState w n)+ landscape . multiWorld %= M.insert swName w'+ return a++-- | Get the entity (if any) at a given location.+entityAt :: (Has (State GameState) sig m) => Cosmic Location -> m (Maybe Entity)+entityAt (Cosmic subworldName loc) =+ join <$> zoomWorld subworldName (W.lookupEntityM @Int (W.locToCoords loc))++-- | Get the robot with a given ID.+robotWithID :: (Has (State GameState) sig m) => RID -> m (Maybe Robot)+robotWithID rid = use (robotMap . at rid)++-- | Get the robot with a given name.+robotWithName :: (Has (State GameState) sig m) => Text -> m (Maybe Robot)+robotWithName rname = use (robotMap . to IM.elems . to (find $ \r -> r ^. robotName == rname))++-- * Randomness++-- | Generate a uniformly random number using the random generator in+-- the game state.+uniform :: (Has (State GameState) sig m, UniformRange a) => (a, a) -> m a+uniform bnds = do+ rand <- use randGen+ let (n, g) = uniformR bnds rand+ randGen .= g+ return n++-- | Given a weighting function and a list of values, choose one of+-- the values randomly (using the random generator in the game+-- state), with the probability of each being proportional to its+-- weight. Return @Nothing@ if the list is empty.+weightedChoice :: Has (State GameState) sig m => (a -> Integer) -> [a] -> m (Maybe a)+weightedChoice weight as = do+ r <- uniform (0, total - 1)+ return $ go r as+ where+ total = sum (map weight as)++ go _ [] = Nothing+ go !k (x : xs)+ | k < w = Just x+ | otherwise = go (k - w) xs+ where+ w = weight x++-- | Generate a random robot name in the form @adjective_name@.+randomName :: Has (State GameState) sig m => m Text+randomName = do+ NameGenerator adjs names <- use $ robotNaming . nameGenerator+ i <- uniform (bounds adjs)+ j <- uniform (bounds names)+ return $ T.concat [adjs ! i, "_", names ! j]++-- * Moving++data MoveFailureMode = PathBlocked | PathLiquid+data MoveFailureDetails = MoveFailureDetails Entity MoveFailureMode++-- | Make sure nothing is in the way.+-- No exception for system robots+checkMoveFailureUnprivileged :: HasRobotStepState sig m => Cosmic Location -> m (Maybe MoveFailureDetails)+checkMoveFailureUnprivileged nextLoc = do+ me <- entityAt nextLoc+ caps <- use robotCapabilities+ unwalkables <- use unwalkableEntities+ return $ do+ e <- me+ go caps unwalkables e+ where+ go caps unwalkables e+ -- robots can not walk through walls+ | e `hasProperty` Unwalkable || (e ^. entityName) `S.member` unwalkables = Just $ MoveFailureDetails e PathBlocked+ -- robots drown if they walk over liquid without boat+ | e `hasProperty` Liquid && CFloat `S.notMember` caps =+ Just $ MoveFailureDetails e PathLiquid+ | otherwise = Nothing++-- | Make sure nothing is in the way. Note that system robots implicitly ignore+-- and base throws on failure.+checkMoveFailure :: HasRobotStepState sig m => Cosmic Location -> m (Maybe MoveFailureDetails)+checkMoveFailure nextLoc = do+ systemRob <- use systemRobot+ runMaybeT $ do+ guard $ not systemRob+ maybeMoveFailure <- lift $ checkMoveFailureUnprivileged nextLoc+ hoistMaybe maybeMoveFailure
src/Swarm/Game/Terrain.hs view
@@ -14,11 +14,12 @@ ) where import Data.Aeson (FromJSON (..), withText)+import Data.List.NonEmpty qualified as NE import Data.Map (Map) import Data.Map qualified as M import Data.Text qualified as T import Swarm.Game.Display-import Swarm.Util (failT)+import Swarm.Util (failT, showEnum) import Text.Read (readMaybe) import Witch (into) @@ -49,7 +50,7 @@ Nothing -> failT ["Unknown terrain type:", t] getTerrainDefaultPaletteChar :: TerrainType -> Char-getTerrainDefaultPaletteChar = head . show+getTerrainDefaultPaletteChar = NE.head . showEnum getTerrainWord :: TerrainType -> T.Text getTerrainWord = T.toLower . T.pack . init . show
src/Swarm/Game/Universe.hs view
@@ -3,6 +3,9 @@ -- | -- SPDX-License-Identifier: BSD-3-Clause+--+-- Types and utilities for working with "universal locations";+-- locations that encompass different 2-D subworlds. module Swarm.Game.Universe where import Control.Lens (makeLenses, view)@@ -14,6 +17,8 @@ import Linear (V2 (..)) import Swarm.Game.Location +-- * Referring to subworlds+ data SubworldName = DefaultRootSubworld | SubworldName Text deriving (Show, Eq, Ord, Generic, ToJSON) @@ -25,9 +30,11 @@ SubworldName s -> s DefaultRootSubworld -> "<default>" +-- * Universal location+ -- | The swarm universe consists of locations -- indexed by subworld.--- Not only is this datatype useful for planar (2D)+-- Not only is this parameterized datatype useful for planar (2D) -- coordinates, but is also used for named waypoints. data Cosmic a = Cosmic { _subworld :: SubworldName@@ -47,17 +54,26 @@ <$> v .: "subworld" <*> v .: "loc" -defaultCosmicLocation :: Cosmic Location-defaultCosmicLocation = Cosmic DefaultRootSubworld origin+-- * Measurement data DistanceMeasure b = Measurable b | InfinitelyFar deriving (Eq, Ord) +getFiniteDistance :: DistanceMeasure b -> Maybe b+getFiniteDistance = \case+ Measurable x -> Just x+ InfinitelyFar -> Nothing+ -- | Returns 'InfinitelyFar' if not within the same subworld. cosmoMeasure :: (a -> a -> b) -> Cosmic a -> Cosmic a -> DistanceMeasure b cosmoMeasure f a b | ((/=) `on` view subworld) a b = InfinitelyFar | otherwise = Measurable $ (f `on` view planar) a b++-- * Utilities++defaultCosmicLocation :: Cosmic Location+defaultCosmicLocation = Cosmic DefaultRootSubworld origin offsetBy :: Cosmic Location -> V2 Int32 -> Cosmic Location offsetBy loc v = fmap (.+^ v) loc
src/Swarm/Game/Value.hs view
@@ -2,10 +2,10 @@ {-# OPTIONS_GHC -fno-warn-orphans #-} -- |+-- SPDX-License-Identifier: BSD-3-Clause+-- -- Conversions from native Haskell values -- to values in the swarm language.------ SPDX-License-Identifier: BSD-3-Clause module Swarm.Game.Value where import Control.Lens (view)@@ -14,6 +14,7 @@ import Swarm.Game.Entity import Swarm.Game.Location import Swarm.Game.Robot+import Swarm.Language.Direction import Swarm.Language.Value -- * Patterns@@ -52,6 +53,9 @@ instance Valuable Robot where asValue = VRobot . view robotID++instance Valuable Direction where+ asValue = VDir instance (Valuable a) => Valuable (Maybe a) where asValue Nothing = VInj False VUnit
src/Swarm/Game/World.hs view
@@ -59,18 +59,18 @@ import Data.Bifunctor (second) import Data.Bits import Data.Foldable (foldl')-import Data.Function (on) import Data.Int (Int32) import Data.Map (Map) import Data.Map.Strict qualified as M import Data.Semigroup (Last (..)) import Data.Yaml (FromJSON, ToJSON) import GHC.Generics (Generic)-import Swarm.Game.Entity (Entity, entityHash)+import Swarm.Game.Entity (Entity) import Swarm.Game.Location import Swarm.Game.Terrain (TerrainType (BlankT)) import Swarm.Game.Universe import Swarm.Game.World.Coords+import Swarm.Game.World.Modify import Swarm.Util ((?)) import Swarm.Util.Erasable import Prelude hiding (lookup)@@ -265,9 +265,9 @@ Coords -> (Maybe Entity -> Maybe Entity) -> World t Entity ->- (World t Entity, Bool)+ (World t Entity, CellUpdate Entity) update i g w@(World f t m) =- (wNew, ((/=) `on` fmap (view entityHash)) entityAfter entityBefore)+ (wNew, classifyModification entityBefore entityAfter) where wNew = World f t $ M.insert i entityAfter m entityBefore = lookupEntity i w@@ -280,7 +280,7 @@ (Has (State (World t Entity)) sig m, IArray U.UArray t) => Coords -> (Maybe Entity -> Maybe Entity) ->- m Bool+ m (CellUpdate Entity) updateM c g = do state @(World t Entity) $ update c g . loadCell c @@ -315,10 +315,11 @@ -- 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.+-- | Enumeration of world updates. This type is used for changes by+-- /e.g./ the @drill@ command which must be carried out at a later+-- tick. Using a first-order representation (as opposed to /e.g./+-- just a @World -> World@ function) allows us to serialize and+-- inspect the updates. data WorldUpdate e = ReplaceEntity { updatedLoc :: Cosmic Location , originalEntity :: e
src/Swarm/Game/World/Coords.hs view
@@ -22,12 +22,12 @@ -- World coordinates ------------------------------------------------------------ --- | World coordinates use (row,column) format, with the row+-- | World coordinates use @(row,column)@ format, with the row -- 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+-- and forth between this type and t'Location', which is used when -- presenting coordinates externally to the player. newtype Coords = Coords {unCoords :: (Int32, Int32)} deriving (Eq, Ord, Show, Ix, Generic)@@ -35,11 +35,11 @@ instance Rewrapped Coords t instance Wrapped Coords --- | Convert an external (x,y) location to an internal 'Coords' value.+-- | Convert an external @(x,y)@ location to an internal 'Coords' value. locToCoords :: Location -> Coords locToCoords (Location x y) = Coords (-y, x) --- | Convert an internal 'Coords' value to an external (x,y) location.+-- | Convert an internal 'Coords' value to an external @(x,y)@ location. coordsToLoc :: Coords -> Location coordsToLoc (Coords (r, c)) = Location c (-r)
+ src/Swarm/Game/World/Modify.hs view
@@ -0,0 +1,40 @@+-- |+-- SPDX-License-Identifier: BSD-3-Clause+--+-- Captures the various possibilities of cell+-- modification as a sum type for use by the structure recognizer+-- (see 'Swarm.Game.Scenario.Topography.Structure.Recognition.Tracking.entityModified').+module Swarm.Game.World.Modify where++import Control.Lens (view)+import Data.Function (on)+import Swarm.Game.Entity (Entity, entityHash)++-- | Compare to 'WorldUpdate' in "Swarm.Game.World"+data CellUpdate e+ = NoChange (Maybe e)+ | Modified (CellModification e)++getModification :: CellUpdate e -> Maybe (CellModification e)+getModification (NoChange _) = Nothing+getModification (Modified x) = Just x++data CellModification e+ = -- | Fields represent what existed in the cell "before" and "after", in that order.+ Swap e e+ | Remove e+ | Add e++classifyModification ::+ -- | before+ Maybe Entity ->+ -- | after+ Maybe Entity ->+ CellUpdate Entity+classifyModification Nothing Nothing = NoChange Nothing+classifyModification Nothing (Just x) = Modified $ Add x+classifyModification (Just x) Nothing = Modified $ Remove x+classifyModification (Just x) (Just y) =+ if ((/=) `on` view entityHash) x y+ then Modified $ Swap x y+ else NoChange $ Just x
src/Swarm/Game/World/Parse.hs view
@@ -10,13 +10,12 @@ -- Parser for the Swarm world description DSL. module Swarm.Game.World.Parse where -import Control.Monad (MonadPlus, void)+import Control.Monad (void) import Control.Monad.Combinators.Expr (Operator (..), makeExprParser)-import Data.List.NonEmpty (NonEmpty)-import Data.List.NonEmpty qualified as NE+import Control.Monad.Combinators.NonEmpty qualified as CNE (sepBy1) import Data.Text (Text) import Data.Text qualified as T-import Data.Void+import Data.Void (Void) import Data.Yaml (FromJSON (parseJSON), withText) import Swarm.Game.World.Syntax import Swarm.Util (failT, showT, squote)@@ -30,12 +29,6 @@ type ParserError = ParseErrorBundle Text Void --------------------------------------------------------------- Utility--sepByNE :: (MonadPlus m) => m a -> m sep -> m (NonEmpty a)-sepByNE p sep = NE.fromList <$> p `sepBy1` sep-------------------------------------------------------------- -- Lexing reservedWords :: [Text]@@ -233,7 +226,7 @@ parseOverlay :: Parser WExp parseOverlay = do reserved "overlay"- brackets $ WOverlay <$> parseWExp `sepByNE` comma+ brackets $ WOverlay <$> parseWExp `CNE.sepBy1` comma parseMask :: Parser WExp parseMask = do
+ src/Swarm/Game/World/Render.hs view
@@ -0,0 +1,65 @@+-- |+-- SPDX-License-Identifier: BSD-3-Clause+--+-- TUI-independent world rendering.+module Swarm.Game.World.Render where++import Control.Effect.Lift (sendIO)+import Control.Lens (view)+import Data.List.NonEmpty qualified as NE+import Swarm.Doc.Gen (loadStandaloneScenario)+import Swarm.Game.Display (defaultChar)+import Swarm.Game.ResourceLoading (initNameGenerator, readAppData)+import Swarm.Game.Scenario (Scenario, area, scenarioWorlds, ul, worldName)+import Swarm.Game.Scenario.Status (emptyLaunchParams)+import Swarm.Game.Scenario.Topography.Area (AreaDimensions (..), getAreaDimensions, isEmpty, upperLeftToBottomRight)+import Swarm.Game.Scenario.Topography.Cell+import Swarm.Game.Scenario.Topography.EntityFacade (EntityFacade (..), mkFacade)+import Swarm.Game.State+import Swarm.Game.Universe+import Swarm.Game.World qualified as W+import Swarm.TUI.Editor.Util (getContentAt, getMapRectangle)+import Swarm.Util.Effect (simpleErrorHandle)+import Swarm.Util.Erasable (erasableToMaybe)++getDisplayChar :: PCell EntityFacade -> Char+getDisplayChar = maybe ' ' facadeChar . erasableToMaybe . cellEntity+ where+ facadeChar (EntityFacade _ d) = view defaultChar d++getDisplayGrid :: Scenario -> GameState -> [[PCell EntityFacade]]+getDisplayGrid myScenario gs =+ getMapRectangle+ mkFacade+ (getContentAt worlds . mkCosmic)+ boundingBox+ where+ worlds = view (landscape . multiWorld) gs++ firstScenarioWorld = NE.head $ view scenarioWorlds myScenario+ worldArea = area firstScenarioWorld+ upperLeftLocation = ul firstScenarioWorld+ rawAreaDims = getAreaDimensions worldArea+ areaDims =+ if isEmpty rawAreaDims+ then AreaDimensions 20 10+ else rawAreaDims+ lowerRightLocation = upperLeftToBottomRight areaDims upperLeftLocation++ mkCosmic = Cosmic $ worldName firstScenarioWorld+ boundingBox = (W.locToCoords upperLeftLocation, W.locToCoords lowerRightLocation)++renderScenarioMap :: FilePath -> IO [String]+renderScenarioMap fp = simpleErrorHandle $ do+ (myScenario, (worldDefs, entities, recipes)) <- loadStandaloneScenario fp+ appDataMap <- readAppData+ nameGen <- initNameGenerator appDataMap+ let gsc = GameStateConfig nameGen entities recipes worldDefs+ gs <- sendIO $ scenarioToGameState myScenario emptyLaunchParams gsc+ let grid = getDisplayGrid myScenario gs++ return $ map (map getDisplayChar) grid++printScenarioMap :: [String] -> IO ()+printScenarioMap =+ sendIO . mapM_ putStrLn
src/Swarm/Game/World/Typecheck.hs view
@@ -367,8 +367,8 @@ ------------------------------------------------------------ -- Instance checking --- | Check that a particular type has an 'Eq' instance, and run a--- computation in a context provided with an 'Eq' constraint. The+-- | Check that a particular type has an 'GHC.Classes.Eq' instance, and run a+-- computation in a context provided with an 'GHC.Classes.Eq' constraint. The -- other @checkX@ functions are similar. checkEq :: (Has (Throw CheckErr) sig m) => TTy ty -> ((Eq ty, NotFun ty) => m a) -> m a checkEq (TTyBase BBool) a = a
src/Swarm/Language/Capability.hs view
@@ -36,6 +36,8 @@ CMove | -- | Execute the 'Backup' command CBackup+ | -- | Execute the 'Path' command+ CPath | -- | Execute the 'Push' command CPush | -- | Execute the 'Stride' command@@ -52,8 +54,12 @@ CGrab | -- | Execute the 'Harvest' command CHarvest+ | -- | Execute the 'Ignite' command+ CIgnite | -- | Execute the 'Place' command CPlace+ | -- | Execute the 'Ping' command+ CPing | -- | Execute the 'Give' command CGive | -- | Execute the 'Equip' command@@ -72,6 +78,8 @@ CSalvage | -- | Execute the 'Drill' command CDrill+ | -- | Execute the 'Waypoint' command+ CWaypoint | -- | Execute the 'Whereami' command CSenseloc | -- | Execute the 'Blocked' command@@ -210,12 +218,15 @@ Selfdestruct -> Just CSelfdestruct Move -> Just CMove Backup -> Just CBackup+ Path -> Just CPath Push -> Just CPush Stride -> Just CMovemultiple Turn -> Just CTurn Grab -> Just CGrab Harvest -> Just CHarvest+ Ignite -> Just CIgnite Place -> Just CPlace+ Ping -> Just CPing Give -> Just CGive Equip -> Just CEquip Unequip -> Just CUnequip@@ -249,7 +260,7 @@ Wait -> Just CTimerel Scout -> Just CRecondir Whereami -> Just CSenseloc- Waypoint -> Just CGod+ Waypoint -> Just CWaypoint Detect -> Just CDetectloc Resonate -> Just CDetectcount Density -> Just CDetectcount
src/Swarm/Language/Direction.hs view
@@ -44,7 +44,7 @@ -- See https://en.wikipedia.org/wiki/Polar_coordinate_system#Conventions -- -- Do not alter this ordering, as there exist functions that depend on it--- (e.g. "nearestDirection" and "relativeTo").+-- (e.g. 'Swarm.Game.Location.nearestDirection' and 'Swarm.Game.Location.relativeTo'). data AbsoluteDir = DEast | DNorth | DWest | DSouth deriving (Eq, Ord, Show, Read, Generic, Data, Hashable, Enum, Bounded) @@ -82,7 +82,7 @@ deriving (Eq, Ord, Show, Read, Generic, Data, Hashable, ToJSON, FromJSON) -- | Caution: Do not alter this ordering, as there exist functions that depend on it--- (e.g. "nearestDirection" and "relativeTo").+-- (e.g. 'Swarm.Game.Location.nearestDirection' and 'Swarm.Game.Location.relativeTo'). data PlanarRelativeDir = DForward | DLeft | DBack | DRight deriving (Eq, Ord, Show, Read, Generic, Data, Hashable, Enum, Bounded) @@ -98,7 +98,7 @@ deriving (Eq, Ord, Show, Read, Generic, Data, Hashable, ToJSON, FromJSON) -- | Direction name is generated from the deepest nested data constructor--- e.g. DLeft becomes "left"+-- e.g. 'DLeft' becomes "left" directionSyntax :: Direction -> Text directionSyntax d = toLower . T.tail . from $ case d of DAbsolute x -> show x@@ -106,7 +106,7 @@ DPlanar y -> show y _ -> show x --- | Check if the direction is absolute (e.g. 'north' or 'south').+-- | Check if the direction is absolute (e.g. 'Swarm.Game.Location.north' or 'Swarm.Game.Location.south'). isCardinal :: Direction -> Bool isCardinal = \case DAbsolute _ -> True
src/Swarm/Language/Key.hs view
@@ -55,7 +55,7 @@ parseKeyComboFull :: Parser KeyCombo parseKeyComboFull = parseKeyCombo <* eof --- | Parse a key combo like "M-C-F5", "Down", or "C-x".+-- | Parse a key combo like @\"M-C-F5\"@, @\"Down\"@, or @\"C-x\"@. parseKeyCombo :: Parser KeyCombo parseKeyCombo = mkKeyCombo <$> many (try (parseModifier <* char '-')) <*> parseKey@@ -115,8 +115,8 @@ ------------------------------------------------------------ -- Pretty-printing --- | Pretty-print a key combo, e.g. "C-M-F5". Right inverse to--- parseKeyCombo. Left inverse up to reordering of modifiers.+-- | Pretty-print a key combo, e.g. @\"C-M-F5\"@. Right inverse to+-- 'parseKeyCombo'. Left inverse up to reordering of modifiers. prettyKeyCombo :: KeyCombo -> Text prettyKeyCombo (KeyCombo k mods) = T.append (T.concat (map prettyModifier mods)) (prettyKey k) @@ -134,4 +134,4 @@ from @String . \case V.KChar c -> [c] V.KFun n -> 'F' : show n- k -> tail (show k)+ k -> drop 1 (show k)
src/Swarm/Language/LSP/Hover.hs view
@@ -1,5 +1,7 @@ {-# LANGUAGE OverloadedStrings #-} +-- |+-- SPDX-License-Identifier: BSD-3-Clause module Swarm.Language.LSP.Hover ( showHoverInfo,
src/Swarm/Language/Parse.hs view
@@ -478,7 +478,7 @@ runParser p t = first (from . errorBundlePretty) (parse (runReaderT p DisallowAntiquoting) "" t) -- | A utility for running a parser in an arbitrary 'MonadFail' (which--- is going to be the TemplateHaskell 'Q' monad --- see+-- is going to be the TemplateHaskell 'Language.Haskell.TH.Q' monad --- see -- "Swarm.Language.Parse.QQ"), with a specified source position. runParserTH :: (Monad m, MonadFail m) => (String, Int, Int) -> Parser a -> String -> m a runParserTH (file, line, col) p s =@@ -489,7 +489,7 @@ -- This is annoying --- megaparsec does not export its function to -- construct an initial parser state, so we can't just use that -- and then change the one field we need to be different (the- -- pstateSourcePos). We have to copy-paste the whole thing.+ -- 'pstateSourcePos'). We have to copy-paste the whole thing. initState :: State Text Void initState = State@@ -509,7 +509,7 @@ -- | Parse some input 'Text' completely as a 'Term', consuming leading -- whitespace and ensuring the parsing extends all the way to the -- end of the input 'Text'. Returns either the resulting 'Term' (or--- @Nothing@ if the input was only whitespace) or a pretty-printed+-- 'Nothing' if the input was only whitespace) or a pretty-printed -- parse error message. readTerm :: Text -> Either Text (Maybe Syntax) readTerm = runParser (fullyMaybe sc parseTerm)@@ -520,7 +520,7 @@ readTerm' = parse (runReaderT (fullyMaybe sc parseTerm) DisallowAntiquoting) "" -- | A utility for converting a ParserError into a one line message:--- <line-nr>: <error-msg>+-- @<line-nr>: <error-msg>@ showShortError :: ParserError -> String showShortError pe = show (line + 1) <> ": " <> from msg where
src/Swarm/Language/Pretty.hs view
@@ -14,6 +14,7 @@ import Control.Unification.IntVar import Data.Bool (bool) import Data.Functor.Fixedpoint (Fix, unFix)+import Data.List.NonEmpty qualified as NE import Data.Map.Strict qualified as M import Data.Set (Set) import Data.Set qualified as S@@ -29,7 +30,7 @@ import Swarm.Language.Syntax import Swarm.Language.Typecheck import Swarm.Language.Types-import Swarm.Util (showLowT)+import Swarm.Util (showEnum, showLowT, unsnocNE) import Witch ------------------------------------------------------------@@ -52,6 +53,10 @@ prettyText :: (PrettyPrec a) => a -> Text prettyText = docToText . ppr +-- | Pretty-print something and render it as (preferably) one line @Text@.+prettyTextLine :: (PrettyPrec a) => a -> Text+prettyTextLine = RT.renderStrict . layoutPretty (LayoutOptions Unbounded) . group . ppr+ -- | Render a pretty-printed document as a @String@. docToString :: Doc a -> String docToString = RS.renderString . layoutPretty defaultLayoutOptions@@ -61,14 +66,18 @@ prettyString = docToString . ppr -- | Optionally surround a document with parentheses depending on the--- @Bool@ argument.+-- @Bool@ argument and if it does not fit on line, indent the lines,+-- with the parens on separate lines. pparens :: Bool -> Doc ann -> Doc ann-pparens True = parens+pparens True = group . encloseWithIndent 2 lparen rparen pparens False = id +encloseWithIndent :: Int -> Doc ann -> Doc ann -> Doc ann -> Doc ann+encloseWithIndent i l r = nest i . enclose (l <> line') (nest (-2) $ line' <> r)+ -- | Surround a document with backticks. bquote :: Doc ann -> Doc ann-bquote d = "`" <> d <> "`"+bquote = group . enclose "`" "`" -- | Turn a 'Show' instance into a @Doc@, lowercasing it in the -- process.@@ -78,6 +87,8 @@ -------------------------------------------------- -- Bullet lists +data Prec a = Prec Int a+ data BulletList i = BulletList { bulletListHeader :: forall a. Doc a , bulletListItems :: [i]@@ -115,6 +126,19 @@ instance PrettyPrec Wildcard where prettyPrec _ _ = "_" +-- | Split a function type chain, so that we can pretty print+-- the type parameters aligned on each line when they don't fit.+class UnchainableFun t where+ unchainFun :: t -> [t]++instance UnchainableFun Type where+ unchainFun (a :->: ty) = a : unchainFun ty+ unchainFun ty = [ty]++instance UnchainableFun (UTerm TypeF ty) where+ unchainFun (UTerm (TyFunF ty1 ty2)) = ty1 : unchainFun ty2+ unchainFun ty = [ty]+ instance (PrettyPrec (t (Fix t))) => PrettyPrec (Fix t) where prettyPrec p = prettyPrec p . unFix @@ -122,7 +146,7 @@ prettyPrec p (UTerm t) = prettyPrec p t prettyPrec p (UVar v) = prettyPrec p v -instance (PrettyPrec t) => PrettyPrec (TypeF t) where+instance ((UnchainableFun t), (PrettyPrec t)) => PrettyPrec (TypeF t) where prettyPrec _ (TyBaseF b) = ppr b prettyPrec _ (TyVarF v) = pretty v prettyPrec p (TySumF ty1 ty2) =@@ -134,8 +158,12 @@ prettyPrec p (TyCmdF ty) = pparens (p > 9) $ "cmd" <+> prettyPrec 10 ty prettyPrec _ (TyDelayF ty) = braces $ ppr ty prettyPrec p (TyFunF ty1 ty2) =- pparens (p > 0) $- prettyPrec 1 ty1 <+> "->" <+> prettyPrec 0 ty2+ let (iniF, lastF) = unsnocNE $ ty1 NE.:| unchainFun ty2+ funs = (prettyPrec 1 <$> iniF) <> [ppr lastF]+ inLine l r = l <+> "->" <+> r+ multiLine l r = l <+> "->" <> hardline <> r+ in pparens (p > 0) . align $+ flatAlt (concatWith multiLine funs) (concatWith inLine funs) prettyPrec _ (TyRcdF m) = brackets $ hsep (punctuate "," (map prettyBinding (M.assocs m))) instance PrettyPrec Polytype where@@ -157,7 +185,7 @@ prettyPrec _ = pretty . directionSyntax instance PrettyPrec Capability where- prettyPrec _ c = pretty $ T.toLower (from (tail $ show c))+ prettyPrec _ c = pretty $ T.toLower (from (NE.tail $ showEnum c)) instance PrettyPrec Const where prettyPrec p c = pparens (p > fixity (constInfo c)) $ pretty . syntax . constInfo $ c@@ -180,10 +208,11 @@ prettyPrec p (TRequire n e) = pparens (p > 10) $ "require" <+> pretty n <+> ppr @Term (TText e) prettyPrec p (TRequirements _ e) = pparens (p > 10) $ "requirements" <+> ppr e prettyPrec _ (TVar s) = pretty s- prettyPrec _ (TDelay _ t) = braces $ ppr t+ prettyPrec _ (TDelay _ t) = group . encloseWithIndent 2 lbrace rbrace $ ppr t prettyPrec _ t@TPair {} = prettyTuple t- prettyPrec _ (TLam x mty body) =- "\\" <> pretty x <> maybe "" ((":" <>) . ppr) mty <> "." <+> ppr body+ prettyPrec p t@(TLam {}) =+ pparens (p > 9) $+ prettyLambdas t -- Special handling of infix operators - ((+) 2) 3 --> 2 + 3 prettyPrec p (TApp t@(TApp (TConst c) l) r) = let ci = constInfo c@@ -207,21 +236,21 @@ _ -> prettyPrecApp p t1 t2 _ -> prettyPrecApp p t1 t2 prettyPrec _ (TLet _ x mty t1 t2) =- hsep $- ["let", pretty x]- ++ maybe [] (\ty -> [":", ppr ty]) mty- ++ ["=", ppr t1, "in", ppr t2]+ sep+ [ prettyDefinition "let" x mty t1 <+> "in"+ , ppr t2+ ] prettyPrec _ (TDef _ x mty t1) =- hsep $- ["def", pretty x]- ++ maybe [] (\ty -> [":", ppr ty]) mty- ++ ["=", ppr t1, "end"]+ sep+ [ prettyDefinition "def" x mty t1+ , "end"+ ] prettyPrec p (TBind Nothing t1 t2) = pparens (p > 0) $- prettyPrec 1 t1 <> ";" <+> prettyPrec 0 t2+ prettyPrec 1 t1 <> ";" <> line <> prettyPrec 0 t2 prettyPrec p (TBind (Just x) t1 t2) = pparens (p > 0) $- pretty x <+> "<-" <+> prettyPrec 1 t1 <> ";" <+> prettyPrec 0 t2+ pretty x <+> "<-" <+> prettyPrec 1 t1 <> ";" <> line <> prettyPrec 0 t2 prettyPrec _ (TRcd m) = brackets $ hsep (punctuate "," (map prettyEquality (M.assocs m))) prettyPrec _ (TProj t x) = prettyPrec 11 t <> "." <> pretty x prettyPrec p (TAnnotate t pt) =@@ -233,11 +262,27 @@ prettyEquality (x, Just t) = pretty x <+> "=" <+> ppr t prettyTuple :: Term -> Doc a-prettyTuple = pparens True . hsep . punctuate "," . map ppr . unnestTuple+prettyTuple = tupled . map ppr . unnestTuple where unnestTuple (TPair t1 t2) = t1 : unnestTuple t2 unnestTuple t = [t] +prettyDefinition :: Doc ann -> Var -> Maybe Polytype -> Term -> Doc ann+prettyDefinition defName x mty t1 =+ nest 2 . sep $+ [ flatAlt+ (defHead <> group defType <+> eqAndLambdaLine)+ (defHead <> group defType' <+> defEqLambdas)+ , ppr defBody+ ]+ where+ (defBody, defLambdaList) = unchainLambdas t1+ defHead = defName <+> pretty x+ defType = maybe "" (\ty -> ":" <+> flatAlt (line <> indent 2 (ppr ty)) (ppr ty)) mty+ defType' = maybe "" (\ty -> ":" <+> ppr ty) mty+ defEqLambdas = hsep ("=" : map prettyLambda defLambdaList)+ eqAndLambdaLine = if null defLambdaList then "=" else line <> defEqLambdas+ prettyPrecApp :: Int -> Term -> Term -> Doc a prettyPrecApp p t1 t2 = pparens (p > 10) $@@ -249,6 +294,19 @@ _ -> appliedTermPrec f appliedTermPrec _ = 10 +prettyLambdas :: Term -> Doc a+prettyLambdas t = hsep (prettyLambda <$> lms) <> softline <> ppr rest+ where+ (rest, lms) = unchainLambdas t++unchainLambdas :: Term -> (Term, [(Var, Maybe Type)])+unchainLambdas = \case+ TLam x mty body -> ((x, mty) :) <$> unchainLambdas body+ body -> (body, [])++prettyLambda :: (Pretty a1, PrettyPrec a2) => (a1, Maybe a2) -> Doc ann+prettyLambda (x, mty) = "\\" <> pretty x <> maybe "" ((":" <>) . ppr) mty <> "."+ ------------------------------------------------------------ -- Error messages @@ -271,37 +329,42 @@ showLoc (r, c) = pretty r <> ":" <> pretty c instance PrettyPrec TypeErr where- prettyPrec _ (UnifyErr ty1 ty2) =- "Can't unify" <+> ppr ty1 <+> "and" <+> ppr ty2- prettyPrec _ (Mismatch Nothing (getJoin -> (ty1, ty2))) =- "Type mismatch: expected" <+> ppr ty1 <> ", but got" <+> ppr ty2- prettyPrec _ (Mismatch (Just t) (getJoin -> (ty1, ty2))) =- nest 2 . vcat $- [ "Type mismatch:"- , "From context, expected" <+> bquote (ppr t) <+> "to" <+> typeDescription Expected ty1 <> ","- , "but it" <+> typeDescription Actual ty2- ]- prettyPrec _ (LambdaArgMismatch (getJoin -> (ty1, ty2))) =- "Lambda argument has type annotation" <+> bquote (ppr ty2) <> ", but expected argument type" <+> bquote (ppr ty1)- prettyPrec _ (FieldsMismatch (getJoin -> (expFs, actFs))) = fieldMismatchMsg expFs actFs- prettyPrec _ (EscapedSkolem x) =- "Skolem variable" <+> pretty x <+> "would escape its scope"- prettyPrec _ (UnboundVar x) =- "Unbound variable" <+> pretty x- prettyPrec _ (Infinite x uty) =- "Infinite type:" <+> ppr x <+> "=" <+> ppr uty- prettyPrec _ (DefNotTopLevel t) =- "Definitions may only be at the top level:" <+> ppr t- prettyPrec _ (CantInfer t) =- "Couldn't infer the type of term (this shouldn't happen; please report this as a bug!):" <+> ppr t- prettyPrec _ (CantInferProj t) =- "Can't infer the type of a record projection:" <+> ppr t- prettyPrec _ (UnknownProj x t) =- "Record does not have a field with name" <+> pretty x <> ":" <+> ppr t- prettyPrec _ (InvalidAtomic reason t) =- "Invalid atomic block:" <+> ppr reason <> ":" <+> ppr t- prettyPrec _ Impredicative =- "Unconstrained unification type variables encountered, likely due to an impredicative type. This is a known bug; for more information see https://github.com/swarm-game/swarm/issues/351 ."+ prettyPrec _ = \case+ UnifyErr ty1 ty2 ->+ "Can't unify" <+> ppr ty1 <+> "and" <+> ppr ty2+ Mismatch Nothing (getJoin -> (ty1, ty2)) ->+ "Type mismatch: expected" <+> ppr ty1 <> ", but got" <+> ppr ty2+ Mismatch (Just t) (getJoin -> (ty1, ty2)) ->+ nest 2 . vcat $+ [ "Type mismatch:"+ , "From context, expected" <+> pprCode t <+> "to" <+> typeDescription Expected ty1 <> ","+ , "but it" <+> typeDescription Actual ty2+ ]+ LambdaArgMismatch (getJoin -> (ty1, ty2)) ->+ "Lambda argument has type annotation" <+> pprCode ty2 <> ", but expected argument type" <+> pprCode ty1+ FieldsMismatch (getJoin -> (expFs, actFs)) ->+ fieldMismatchMsg expFs actFs+ EscapedSkolem x ->+ "Skolem variable" <+> pretty x <+> "would escape its scope"+ UnboundVar x ->+ "Unbound variable" <+> pretty x+ Infinite x uty ->+ "Infinite type:" <+> ppr x <+> "=" <+> ppr uty+ DefNotTopLevel t ->+ "Definitions may only be at the top level:" <+> pprCode t+ CantInfer t ->+ "Couldn't infer the type of term (this shouldn't happen; please report this as a bug!):" <+> pprCode t+ CantInferProj t ->+ "Can't infer the type of a record projection:" <+> pprCode t+ UnknownProj x t ->+ "Record does not have a field with name" <+> pretty x <> ":" <+> pprCode t+ InvalidAtomic reason t ->+ "Invalid atomic block:" <+> ppr reason <> ":" <+> pprCode t+ Impredicative ->+ "Unconstrained unification type variables encountered, likely due to an impredicative type. This is a known bug; for more information see https://github.com/swarm-game/swarm/issues/351 ."+ where+ pprCode :: PrettyPrec a => a -> Doc ann+ pprCode = bquote . ppr -- | Given a type and its source, construct an appropriate description -- of it to go in a type mismatch error message.@@ -371,7 +434,7 @@ instance PrettyPrec InvalidAtomicReason where prettyPrec _ (TooManyTicks n) = "block could take too many ticks (" <> pretty n <> ")" prettyPrec _ AtomicDupingThing = "def, let, and lambda are not allowed"- prettyPrec _ (NonSimpleVarType _ ty) = "reference to variable with non-simple type" <+> ppr ty+ prettyPrec _ (NonSimpleVarType _ ty) = "reference to variable with non-simple type" <+> ppr (prettyTextLine ty) prettyPrec _ NestedAtomic = "nested atomic block" prettyPrec _ LongConst = "commands that can take multiple ticks to execute are not allowed"
src/Swarm/Language/Requirement.hs view
@@ -51,10 +51,10 @@ ReqCap Capability | -- | 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- -- will be looked up at runtime, when actually executing a 'Build'- -- or 'Reprogram' command, and an appropriate exception thrown if+ -- 'Swarm.Game.Entity.Entity'. That's because programs have to be type- and+ -- capability-checked independent of an 'Swarm.Game.Entity.EntityMap'. The name+ -- will be looked up at runtime, when actually executing a 'Swarm.Language.Syntax.Build'+ -- or 'Swarm.Language.Syntax.Reprogram' command, and an appropriate exception thrown if -- a device with the given name does not exist. -- -- Requiring the same device multiple times is the same as@@ -62,10 +62,10 @@ ReqDev Text | -- | Require a certain number of a specific entity to be available -- in the inventory. The same comments apply re: resolving the- -- entity name to an actual 'Entity'.+ -- entity name to an actual 'Swarm.Game.Entity.Entity'. -- -- Inventory requirements are additive, that is, say, requiring 5- -- of entity `e` and later requiring 7 is the same as requiring+ -- of entity @"e"@ and later requiring 7 is the same as requiring -- 12. ReqInv Int Text deriving (Eq, Ord, Show, Read, Generic, Hashable, Data, FromJSON, ToJSON)
src/Swarm/Language/Syntax.hs view
@@ -40,6 +40,7 @@ maxSniffRange, maxScoutRange, maxStrideRange,+ maxPathRange, -- * Syntax Syntax' (..),@@ -75,7 +76,6 @@ unfoldApps, -- * Erasure- erase, eraseS, -- * Term traversal@@ -89,6 +89,7 @@ ) where import Control.Lens (Plated (..), Traversal', makeLenses, para, universe, (%~), (^.))+import Control.Monad (void) import Data.Aeson.Types hiding (Key) import Data.Data (Data) import Data.Data.Lens (uniplate)@@ -108,7 +109,7 @@ import Witch.From (from) -- | Maximum perception distance for--- 'chirp' and 'sniff' commands+-- 'Chirp' and 'Sniff' commands maxSniffRange :: Int32 maxSniffRange = 256 @@ -118,6 +119,9 @@ maxStrideRange :: Int maxStrideRange = 64 +maxPathRange :: Integer+maxPathRange = 128+ ------------------------------------------------------------ -- Constants ------------------------------------------------------------@@ -152,6 +156,8 @@ Move | -- | Move backward one step. Backup+ | -- | Describe a path to the destination.+ Path | -- | Push an entity forward one step. Push | -- | Move forward multiple steps.@@ -162,8 +168,12 @@ Grab | -- | Harvest an item from the current location. Harvest+ | -- | Ignite a combustible item+ Ignite | -- | Try to place an item at the current location. Place+ | -- | Obtain the relative location of another robot.+ Ping | -- | Give an item to another robot at the current location. Give | -- | Equip a device on oneself.@@ -370,7 +380,7 @@ RobotNumbered | -- | Check if an entity is known. Knows- deriving (Eq, Ord, Enum, Bounded, Data, Show, Generic, FromJSON, ToJSON)+ deriving (Eq, Ord, Enum, Bounded, Data, Show, Generic, FromJSON, ToJSON, FromJSONKey, ToJSONKey) allConst :: [Const] allConst = Util.listEnums@@ -523,6 +533,12 @@ ] Move -> command 0 short "Move forward one step." Backup -> command 0 short "Move backward one step."+ Path ->+ command 2 short . doc "Obtain shortest path to the destination." $+ [ "Optionally supply a distance limit as the first argument."+ , "Supply either a location (`inL`) or an entity (`inR`) as the second argument."+ , "If a path exists, returns the direction to proceed along."+ ] Push -> command 1 short . doc "Push an entity forward one step." $ [ "Both entity and robot moves forward one step."@@ -539,9 +555,19 @@ [ "Leaves behind a growing seed if the harvested item is growable." , "Otherwise it works exactly like `grab`." ]+ Ignite ->+ command 1 short . doc "Ignite a combustible item in the specified direction." $+ [ "Combustion persists for a random duration and may spread."+ ] Place -> command 1 short . doc "Place an item at the current location." $ ["The current location has to be empty for this to work."]+ Ping ->+ command 1 short . doc "Obtain the relative location of another robot." $+ [ "The other robot must be within transmission range, accounting for antennas installed on either end, and the invoking robot must be oriented in a cardinal direction."+ , "The location (x, y) is given relative to one's current orientation:"+ , "Positive x value is to the right, negative left. Likewise, positive y value is forward, negative back."+ ] 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."@@ -898,16 +924,25 @@ SProj (Syntax' ty) Var | -- | Annotate a term with a type SAnnotate (Syntax' ty) Polytype- deriving (Eq, Show, Functor, Foldable, Traversable, Data, Generic, FromJSON, ToJSON)---- 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.+ deriving+ ( Eq+ , Show+ , Functor+ , Foldable+ , Data+ , Generic+ , FromJSON+ , ToJSON+ , -- | 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.+ Traversable+ ) type Term = Term' () @@ -1059,37 +1094,10 @@ -------------------------------------------------- -- Erasure --- | Erase a 'Syntax' tree annotated with @SrcLoc@ and type+-- | Erase a 'Syntax' tree annotated with 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 (SRequirements x s) = TRequirements x (eraseS s)-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)-erase (SRcd m) = TRcd ((fmap . fmap) eraseS m)-erase (SProj s x) = TProj (eraseS s) x-erase (SAnnotate s pty) = TAnnotate (eraseS s) pty+eraseS (Syntax' _ t _) = void t ------------------------------------------------------------ -- Free variable traversals
src/Swarm/Language/Text/Markdown.hs view
@@ -40,7 +40,6 @@ import Control.Applicative ((<|>)) import Control.Arrow (left) import Control.Lens ((%~), (&), _head, _last)-import Control.Monad (void) import Data.Char (isSpace) import Data.Functor.Identity (Identity (..)) import Data.List.Split (chop)@@ -53,10 +52,9 @@ import Data.Vector (toList) import Data.Yaml import GHC.Exts qualified (IsList (..), IsString (..))-import Swarm.Language.Module (moduleAST) import Swarm.Language.Parse (readTerm)-import Swarm.Language.Pipeline (ProcessedTerm (..), processParsedTerm)-import Swarm.Language.Pretty (PrettyPrec (..), prettyText, prettyTypeErrText)+import Swarm.Language.Pipeline (processParsedTerm)+import Swarm.Language.Pretty (PrettyPrec (..), prettyText, prettyTextLine, prettyTypeErrText) import Swarm.Language.Syntax (Syntax) -- | The top-level markdown document.@@ -69,7 +67,7 @@ -- The idea is that paragraphs do not have line breaks, -- and so the inline elements follow each other. -- In particular inline code can be followed by text without--- space between them (e.g. `logger`s).+-- space between them (e.g. @\`logger\`s@). newtype Paragraph c = Paragraph {nodes :: [Node c]} deriving (Eq, Show, Functor, Foldable, Traversable) deriving (Semigroup, Monoid) via [Node c]@@ -173,8 +171,11 @@ Left e -> Left (T.unpack e) Right Nothing -> Left "empty code" Right (Just s) -> case processParsedTerm s of+ -- Just run the typechecker etc. to make sure the term typechecks Left e -> Left (T.unpack $ prettyTypeErrText t e)- Right (ProcessedTerm modul _req _reqCtx) -> Right $ void $ moduleAST modul+ -- ...but if it does, we just go back to using the original parsed+ -- (*unelaborated*) AST. See #1496.+ Right _ -> Right s findCode :: Document Syntax -> [Syntax] findCode = catMaybes . concatMap (map codeOnly . nodes) . paragraphs@@ -309,7 +310,7 @@ instance PrettyPrec a => ToStream (Node a) where toStream = \case LeafText a t -> [TextNode a t]- LeafCode t -> [CodeNode (prettyText t)]+ LeafCode t -> [CodeNode (prettyTextLine t)] LeafRaw s t -> [RawNode s t] LeafCodeBlock _i t -> [CodeNode (prettyText t)]
src/Swarm/Language/Typecheck.hs view
@@ -161,6 +161,12 @@ -- monad transformer provided by the @unification-fd@ library which -- supports various operations such as generating fresh variables -- and unifying things.+--+-- Note that we are sort of constrained to use a concrete monad stack by+-- @unification-fd@, which has some strange types on some of its exported+-- functions that actually require various monad transformers to be stacked+-- in certain ways. For example, see <https://hackage.haskell.org/package/unification-fd-0.11.2/docs/Control-Unification.html#v:unify>. I don't really see a way+-- to use "capability style" like we do elsewhere in the codebase. type TC = ReaderT UCtx (ReaderT TCStack (ExceptT ContextualTypeErr (IntBindingT TypeF Identity))) -- | Push a frame on the typechecking stack within a local 'TC'@@ -542,6 +548,21 @@ Module c1' ctx1 <- withFrame l TCBindL $ inferModule c1 a <- decomposeCmdTy c1 (Actual, c1' ^. sType) + -- Note we generalize here, similar to how we generalize at let+ -- bindings, since the result type of the LHS will be the type of+ -- the variable (if there is one). In many cases this doesn't+ -- matter, but variables bound by top-level bind expressions can+ -- end up in the top-level context (e.g. if someone writes `x <-+ -- blah` at the REPL). We must generalize here, before adding the+ -- variable to the context, since afterwards it will be too late:+ -- we cannot generalize over any unification variables occurring+ -- in the context.+ --+ -- This is safe since it is always safe to generalize at any point.+ --+ -- See #351, #1501.+ genA <- generalize a+ -- Now infer the right side under an extended context: things in -- scope on the right-hand side include both any definitions -- created by the left-hand side, as well as a variable as in @x@@ -550,7 +571,7 @@ -- 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) . lvVar) mx $ do+ maybe id ((`withBinding` genA) . lvVar) mx $ do Module c2' ctx2 <- withFrame l TCBindR $ inferModule c2 -- We don't actually need the result type since we're just@@ -564,7 +585,7 @@ -- (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) . lvVar) mx+ let ctxX = maybe Ctx.empty ((`Ctx.singleton` genA) . lvVar) mx return $ Module (Syntax' l (SBind mx c1' c2') (c2' ^. sType))@@ -665,8 +686,9 @@ SBind mx c1 c2 -> do c1' <- withFrame l TCBindL $ infer c1 a <- decomposeCmdTy c1 (Actual, c1' ^. sType)+ genA <- generalize a c2' <-- maybe id ((`withBinding` Forall [] a) . lvVar) mx+ maybe id ((`withBinding` genA) . lvVar) mx . withFrame l TCBindR $ infer c2 _ <- decomposeCmdTy c2 (Actual, c2' ^. sType)@@ -720,12 +742,15 @@ Selfdestruct -> [tyQ| cmd unit |] Move -> [tyQ| cmd unit |] Backup -> [tyQ| cmd unit |]+ Path -> [tyQ| (unit + int) -> ((int * int) + text) -> cmd (unit + dir) |] Push -> [tyQ| cmd unit |] Stride -> [tyQ| int -> cmd unit |] Turn -> [tyQ| dir -> cmd unit |] Grab -> [tyQ| cmd text |] Harvest -> [tyQ| cmd text |]+ Ignite -> [tyQ| dir -> cmd unit |] Place -> [tyQ| text -> cmd unit |]+ Ping -> [tyQ| actor -> cmd (unit + (int * int)) |] Give -> [tyQ| actor -> text -> cmd unit |] Equip -> [tyQ| text -> cmd unit |] Unequip -> [tyQ| text -> cmd unit |]
+ src/Swarm/Log.hs view
@@ -0,0 +1,70 @@+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TemplateHaskell #-}++-- |+-- SPDX-License-Identifier: BSD-3-Clause+--+-- A data type to represent log messages, both for robot logs and+-- the system log.+module Swarm.Log (+ Severity (..),+ RobotLogSource (..),+ LogSource (..),+ LogEntry (..),+ leTime,+ leSource,+ leSeverity,+ leName,+ leText,+) where++import Control.Lens (makeLenses)+import Data.Aeson (FromJSON, ToJSON)+import Data.Text (Text)+import GHC.Generics (Generic)+import Swarm.Game.CESK (TickNumber)+import Swarm.Game.Location (Location)+import Swarm.Game.Universe (Cosmic)++-- | Severity of the error - critical errors are bugs+-- and should be reported as Issues.+data Severity = Info | Debug | Warning | Error | Critical+ deriving (Show, Eq, Ord, Generic, FromJSON, ToJSON)++-- | How a robot log entry was produced.+data RobotLogSource+ = -- | Produced by 'Swarm.Language.Syntax.Say'+ Said+ | -- | Produced by 'Swarm.Language.Syntax.Log'+ Logged+ | -- | Produced as the result of an error.+ RobotError+ deriving (Show, Eq, Ord, Generic, FromJSON, ToJSON)++-- | Source of a log entry.+data LogSource+ = -- | Log produced by a robot. Stores information about which+ -- command was used and the ID and location of the producing+ -- robot.+ RobotLog RobotLogSource Int (Cosmic Location)+ | -- | Log produced by an exception or system.+ SystemLog+ deriving (Show, Eq, Ord, Generic, FromJSON, ToJSON)++-- | A log entry.+data LogEntry = LogEntry+ { _leTime :: TickNumber+ -- ^ The time at which the entry was created.+ -- Note that this is the first field we sort on.+ , _leSource :: LogSource+ -- ^ Where this log message came from.+ , _leSeverity :: Severity+ -- ^ Severity level of this log message.+ , _leName :: Text+ -- ^ Name of the robot or subsystem that generated this log entry.+ , _leText :: Text+ -- ^ The text of the log entry.+ }+ deriving (Show, Eq, Ord, Generic, FromJSON, ToJSON)++makeLenses ''LogEntry
src/Swarm/ReadableIORef.hs view
@@ -1,7 +1,7 @@ -- | -- SPDX-License-Identifier: BSD-3-Clause ----- Ensures that access to an IORef is read-only+-- Ensures that access to an 'IORef' is read-only -- by hiding behind a newtype. module Swarm.ReadableIORef (mkReadonly, ReadableIORef, readIORef) where
− src/Swarm/TUI/Attr.hs
@@ -1,189 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}---- |--- SPDX-License-Identifier: BSD-3-Clause------ Rendering attributes (/i.e./ foreground and background colors,--- styles, /etc./) used by the Swarm TUI.------ We export constants only for those we use in the Haskell code--- 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 exception.-module Swarm.TUI.Attr (- swarmAttrMap,- worldAttributes,- worldPrefix,- toAttrName,-- -- ** Terrain attributes- dirtAttr,- grassAttr,- stoneAttr,- waterAttr,- iceAttr,-- -- ** Common attributes- entityAttr,- robotAttr,- rockAttr,- plantAttr,-- -- ** Swarm TUI Attributes- highlightAttr,- notifAttr,- infoAttr,- boldAttr,- italicAttr,- dimAttr,- magentaAttr,- cyanAttr,- lightCyanAttr,- yellowAttr,- blueAttr,- greenAttr,- redAttr,- defAttr,- customEditFocusedAttr,-) where--import Brick-import Brick.Forms-import Brick.Widgets.Dialog-import Brick.Widgets.Edit qualified as E-import Brick.Widgets.List-import Data.Bifunctor (bimap)-import Data.Text (unpack)-import Graphics.Vty qualified as V-import Swarm.Game.Display (Attribute (..))--toAttrName :: Attribute -> AttrName-toAttrName = \case- ARobot -> robotAttr- AEntity -> entityAttr- AWorld n -> worldPrefix <> attrName (unpack n)- ATerrain n -> terrainPrefix <> attrName (unpack n)- ADefault -> defAttr---- | A mapping from the defined attribute names to TUI attributes.-swarmAttrMap :: AttrMap-swarmAttrMap =- attrMap- V.defAttr- $ worldAttributes- <> [(waterAttr, V.white `on` V.blue)]- <> terrainAttr- <> [ -- Robot attribute- (robotAttr, fg V.white `V.withStyle` V.bold)- , -- UI rendering attributes- (highlightAttr, fg V.cyan)- , (invalidFormInputAttr, fg V.red)- , (focusedFormInputAttr, V.defAttr)- , (customEditFocusedAttr, V.black `on` V.yellow)- , (listSelectedFocusedAttr, bg V.blue)- , (infoAttr, fg (V.rgbColor @Int 50 50 50))- , (buttonSelectedAttr, bg V.blue)- , (notifAttr, fg V.yellow `V.withStyle` V.bold)- , (dimAttr, V.defAttr `V.withStyle` V.dim)- , (boldAttr, V.defAttr `V.withStyle` V.bold)- , (italicAttr, V.defAttr `V.withStyle` V.italic)- , -- Basic colors- (redAttr, fg V.red)- , (greenAttr, fg V.green)- , (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)- ]--entityAttr :: AttrName-entityAttr = fst $ head worldAttributes--worldPrefix :: AttrName-worldPrefix = attrName "world"---- | Colors of entities in the world.------ Also used to color messages, so water is special and excluded.-worldAttributes :: [(AttrName, V.Attr)]-worldAttributes =- bimap ((worldPrefix <>) . attrName) fg- <$> [ ("entity", V.white)- , ("device", V.brightYellow)- , ("plant", V.green)- , ("rock", V.rgbColor @Int 80 80 80)- , ("wood", V.rgbColor @Int 139 69 19)- , ("flower", V.rgbColor @Int 200 0 200)- , ("rubber", V.rgbColor @Int 245 224 179)- , ("copper", V.yellow)- , ("copper'", V.rgbColor @Int 78 117 102)- , ("iron", V.rgbColor @Int 97 102 106)- , ("iron'", V.rgbColor @Int 183 65 14)- , ("quartz", V.white)- , ("silver", V.rgbColor @Int 192 192 192)- , ("gold", V.rgbColor @Int 255 215 0)- , ("snow", V.white)- , ("sand", V.rgbColor @Int 194 178 128)- , ("fire", V.brightRed)- , ("red", V.red)- , ("green", V.green)- , ("blue", V.blue)- ]--terrainPrefix :: AttrName-terrainPrefix = attrName "terrain"--terrainAttr :: [(AttrName, V.Attr)]-terrainAttr =- [ (dirtAttr, fg (V.rgbColor @Int 165 42 42))- , (grassAttr, fg (V.rgbColor @Int 0 32 0)) -- dark green- , (stoneAttr, fg (V.rgbColor @Int 32 32 32))- , (iceAttr, bg V.white)- ]---- | The default robot attribute.-robotAttr :: AttrName-robotAttr = attrName "robot"--dirtAttr, grassAttr, stoneAttr, iceAttr, waterAttr, rockAttr, plantAttr :: AttrName-dirtAttr = terrainPrefix <> attrName "dirt"-grassAttr = terrainPrefix <> attrName "grass"-stoneAttr = terrainPrefix <> attrName "stone"-iceAttr = terrainPrefix <> attrName "ice"-waterAttr = worldPrefix <> attrName "water"-rockAttr = worldPrefix <> attrName "rock"-plantAttr = worldPrefix <> attrName "plant"---- | Some defined attribute names used in the Swarm TUI.-highlightAttr- , notifAttr- , infoAttr- , boldAttr- , italicAttr- , dimAttr- , defAttr ::- AttrName-highlightAttr = attrName "highlight"-notifAttr = attrName "notif"-infoAttr = attrName "info"-boldAttr = attrName "bold"-italicAttr = attrName "italics"-dimAttr = attrName "dim"-defAttr = attrName "def"--customEditFocusedAttr :: AttrName-customEditFocusedAttr = attrName "custom" <> E.editFocusedAttr---- | Some basic colors used in TUI.-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"
src/Swarm/TUI/Controller.hs view
@@ -10,7 +10,7 @@ handleEvent, quitGame, - -- ** Handling 'Frame' events+ -- ** Handling 'Swarm.TUI.Model.Frame' events runFrameUI, runFrame, ticksPerFrameCap,@@ -92,6 +92,7 @@ import Swarm.Language.Typed (Typed (..)) import Swarm.Language.Types import Swarm.Language.Value (Value (VKey, VUnit), prettyValue, stripVResult)+import Swarm.Log import Swarm.TUI.Controller.Util import Swarm.TUI.Editor.Controller qualified as EC import Swarm.TUI.Editor.Model@@ -134,10 +135,14 @@ handleEvent = \case -- the query for upstream version could finish at any time, so we have to handle it here AppEvent (UpstreamVersion ev) -> do- let logReleaseEvent l e = runtimeState . eventLog %= logEvent l ("Release", -7) (T.pack $ show e)+ let logReleaseEvent l sev e = runtimeState . eventLog %= logEvent l sev "Release" (T.pack $ show e) case ev of- Left e@(FailedReleaseQuery _e) -> logReleaseEvent (ErrorTrace Error) e- Left e -> logReleaseEvent (ErrorTrace Warning) e+ Left e ->+ let sev = case e of+ FailedReleaseQuery {} -> Error+ OnDevelopmentBranch {} -> Info+ _ -> Warning+ in logReleaseEvent SystemLog sev e Right _ -> pure () runtimeState . upstreamRelease .= ev e -> do@@ -172,7 +177,7 @@ NewGame -> do cheat <- use $ uiState . uiCheatMode ss <- use $ runtimeState . scenarios- uiState . uiMenu .= NewGameMenu (NE.fromList [mkScenarioList cheat ss])+ uiState . uiMenu .= NewGameMenu (pure $ mkScenarioList cheat ss) Tutorial -> do -- Set up the menu stack as if the user had chosen "New Game > Tutorials" cheat <- use $ uiState . uiCheatMode@@ -183,7 +188,7 @@ ((== tutorialsDirname) . T.unpack . scenarioItemName) (mkScenarioList cheat ss) tutorialMenu = mkScenarioList cheat tutorialCollection- menuStack = NE.fromList [tutorialMenu, topMenu]+ menuStack = tutorialMenu :| pure topMenu uiState . uiMenu .= NewGameMenu menuStack -- Extract the first tutorial challenge and run it@@ -253,7 +258,7 @@ Key V.KEnter -> case snd <$> BL.listSelectedElement curMenu of Nothing -> continueWithoutRedraw- Just (SISingle siPair) -> startGame siPair Nothing+ Just (SISingle siPair) -> invalidateCache >> startGame siPair Nothing Just (SICollection _ c) -> do cheat <- use $ uiState . uiCheatMode uiState . uiMenu .= NewGameMenu (NE.cons (mkScenarioList cheat c) scenarioStack)@@ -289,13 +294,13 @@ s <- get mt <- preuse $ uiState . uiModal . _Just . modalType let isRunning = maybe True isRunningModal mt- let isPaused = s ^. gameState . paused+ let isPaused = s ^. gameState . temporal . paused let isCreative = s ^. gameState . creativeMode let hasDebug = fromMaybe isCreative $ s ^? gameState . to focusedRobot . _Just . robotCapabilities . Lens.contains CDebug case ev of AppEvent ae -> case ae of Frame- | s ^. gameState . paused -> continueWithoutRedraw+ | s ^. gameState . temporal . paused -> continueWithoutRedraw | otherwise -> runFrameUI Web (RunWebCode c) -> runBaseWebCode c _ -> continueWithoutRedraw@@ -305,28 +310,27 @@ WinConditions (Won _) _ -> toggleModal $ ScenarioEndModal WinModal WinConditions (Unwinnable _) _ -> toggleModal $ ScenarioEndModal LoseModal _ -> toggleModal QuitModal- VtyEvent (V.EvResize _ _) -> invalidateCacheEntry WorldCache+ VtyEvent (V.EvResize _ _) -> invalidateCache 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 case m ^. modalType of MessagesModal -> do- gameState . lastSeenMessageTime .= s ^. gameState . ticks+ gameState . messageInfo . lastSeenMessageTime .= s ^. gameState . temporal . ticks _ -> return () FKey 1 -> toggleModal HelpModal FKey 2 -> toggleModal RobotsModal- FKey 3 | not (null (s ^. gameState . availableRecipes . notificationsContent)) -> do+ FKey 3 | not (null (s ^. gameState . discovery . availableRecipes . notificationsContent)) -> do toggleModal RecipesModal- gameState . availableRecipes . notificationsCount .= 0- FKey 4 | not (null (s ^. gameState . availableCommands . notificationsContent)) -> do+ gameState . discovery . availableRecipes . notificationsCount .= 0+ FKey 4 | not (null (s ^. gameState . discovery . availableCommands . notificationsContent)) -> do toggleModal CommandsModal- gameState . availableCommands . notificationsCount .= 0+ gameState . discovery . availableCommands . notificationsCount .= 0 FKey 5 | not (null (s ^. gameState . messageNotifications . notificationsContent)) -> do toggleModal MessagesModal- gameState . lastSeenMessageTime .= s ^. gameState . ticks+ gameState . messageInfo . lastSeenMessageTime .= s ^. gameState . temporal . ticks -- show goal ControlChar 'g' -> if hasAnythingToShow $ s ^. uiState . uiGoal . goalsContent@@ -347,12 +351,12 @@ MetaChar 'd' | isPaused && hasDebug -> do debug <- uiState . uiShowDebug Lens.<%= not if debug- then gameState . gameStep .= RobotStep SBefore+ then gameState . temporal . gameStep .= RobotStep SBefore else zoomGameState finishGameTick >> void updateUI -- pausing and stepping ControlChar 'p' | isRunning -> safeTogglePause ControlChar 'o' | isRunning -> do- gameState . runStatus .= ManualPause+ gameState . temporal . runStatus .= ManualPause runGameTickUI -- speed controls ControlChar 'x' | isRunning -> modify $ adjustTPS (+)@@ -376,7 +380,7 @@ -- toggle world editor mode if in "cheat mode" ControlChar 'e' | s ^. uiState . uiCheatMode -> do- uiState . uiWorldEditor . isWorldEditorEnabled %= not+ uiState . uiWorldEditor . worldOverdraw . isWorldEditorEnabled %= not setFocus WorldEditorPanel MouseDown WorldPositionIndicator _ _ _ -> uiState . uiWorldCursor .= Nothing MouseDown (FocusablePanel WorldPanel) V.BMiddle _ mouseLoc ->@@ -445,7 +449,7 @@ curTime <- liftIO $ getTime Monotonic uiState . lastFrameTime .= curTime uiState . uiShowDebug .= False- p <- gameState . runStatus Lens.<%= toggleRunStatus+ p <- gameState . temporal . runStatus Lens.<%= toggleRunStatus when (p == Running) $ zoomGameState finishGameTick -- | Only unpause the game if leaving autopaused modal.@@ -454,7 +458,7 @@ -- the modal, in that case, leave the game paused. safeAutoUnpause :: EventM Name AppState () safeAutoUnpause = do- runs <- use $ gameState . runStatus+ runs <- use $ gameState . temporal . runStatus when (runs == AutoPause) safeTogglePause toggleModal :: ModalType -> EventM Name AppState ()@@ -472,8 +476,13 @@ 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) -> quitGame >> startGame siPair Nothing+ Just (Button StartOverButton, StartOver currentSeed siPair) -> do+ invalidateCache+ restartGame currentSeed siPair+ Just (Button NextButton, Next siPair) -> do+ quitGame+ invalidateCache+ startGame siPair Nothing _ -> return () ev -> do Brick.zoom (uiState . uiModal . _Just . modalDialog) (handleDialogEvent ev)@@ -511,13 +520,13 @@ saveScenarioInfoOnFinish :: (MonadIO m, MonadState AppState m) => FilePath -> m (Maybe ScenarioInfo) saveScenarioInfoOnFinish p = do- initialRunCode <- use $ gameState . initiallyRunCode+ initialRunCode <- use $ gameState . gameControls . initiallyRunCode t <- liftIO getZonedTime wc <- use $ gameState . winCondition let won = case wc of WinConditions (Won _) _ -> True _ -> False- ts <- use $ gameState . ticks+ ts <- use $ gameState . temporal . ticks -- NOTE: This traversal is apparently not the same one as used by -- the scenario selection menu, so the menu needs to be updated separately.@@ -637,7 +646,7 @@ -- | Run the game for a single frame, without updating the UI. runFrame :: EventM Name AppState () runFrame = do- -- Reset the needsRedraw flag. While procssing the frame and stepping the robots,+ -- Reset the needsRedraw flag. While processing the frame and stepping the robots, -- the flag will get set to true if anything changes that requires redrawing the -- world (e.g. a robot moving or disappearing). gameState . needsRedraw .= False@@ -737,7 +746,7 @@ updateAchievements :: EventM Name AppState () updateAchievements = do -- Merge the in-game achievements with the master list in UIState- achievementsFromGame <- use $ gameState . gameAchievements+ achievementsFromGame <- use $ gameState . discovery . gameAchievements let wrappedGameAchievements = M.mapKeys GameplayAchievement achievementsFromGame oldMasterAchievementsList <- use $ uiState . uiAchievements@@ -783,7 +792,7 @@ -- Whether the focused robot is too far away to sense, & whether -- that has recently changed dist <- use (gameState . to focusedRange)- farOK <- liftA2 (||) (use (gameState . creativeMode)) (use (gameState . worldScrollable))+ farOK <- liftA2 (||) (use (gameState . creativeMode)) (use (gameState . landscape . worldScrollable)) let tooFar = not farOK && dist == Just Far farChanged = tooFar /= isNothing listRobotHash @@ -800,10 +809,10 @@ else pure False -- Now check if the base finished running a program entered at the REPL.- replUpdated <- case g ^. replStatus of+ replUpdated <- case g ^. gameControls . replStatus of -- It did, and the result was the unit value. Just reset replStatus. REPLWorking (Typed (Just VUnit) typ reqs) -> do- gameState . replStatus .= REPLDone (Just $ Typed VUnit typ reqs)+ gameState . gameControls . replStatus .= REPLDone (Just $ Typed VUnit typ reqs) pure True -- It did, and returned some other value. Pretty-print the@@ -811,13 +820,15 @@ REPLWorking (Typed (Just v) pty reqs) -> do let finalType = stripCmd pty let val = Typed (stripVResult v) finalType reqs- itIx <- use (gameState . replNextValueIndex)+ itIx <- use (gameState . gameControls . replNextValueIndex) let itName = fromString $ "it" ++ show itIx let out = T.intercalate " " [itName, ":", prettyText finalType, "=", into (prettyValue v)] uiState . uiREPL . replHistory %= addREPLItem (REPLOutput out)- gameState . replStatus .= REPLDone (Just val)+ invalidateCacheEntry REPLHistoryCache+ vScrollToEnd replScroll+ gameState . gameControls . replStatus .= REPLDone (Just val) gameState . baseRobot . robotContext . at itName .= Just val- gameState . replNextValueIndex %= (+ 1)+ gameState . gameControls . replNextValueIndex %= (+ 1) pure True -- Otherwise, do nothing.@@ -856,23 +867,6 @@ uiState . uiScrollToEnd .= True pure True - -- Decide whether the info panel has more content scrolled off the- -- top and/or bottom, so we can draw some indicators to show it if- -- so. Note, because we only know the update size and position of- -- the viewport *after* it has been rendered, this means the top and- -- bottom indicators will only be updated one frame *after* the info- -- panel updates, but this isn't really that big of deal.- infoPanelUpdated <- do- mvp <- lookupViewport InfoViewport- case mvp of- Nothing -> return False- Just vp -> do- let topMore = (vp ^. vpTop) > 0- botMore = (vp ^. vpTop + snd (vp ^. vpSize)) < snd (vp ^. vpContentSize)- oldTopMore <- uiState . uiMoreInfoTop <<.= topMore- oldBotMore <- uiState . uiMoreInfoBot <<.= botMore- return $ oldTopMore /= topMore || oldBotMore /= botMore- goalOrWinUpdated <- doGoalUpdates let redraw =@@ -880,7 +874,6 @@ || inventoryUpdated || replUpdated || logUpdated- || infoPanelUpdated || goalOrWinUpdated pure redraw @@ -901,7 +894,7 @@ curGoal <- use (uiState . uiGoal . goalsContent) isCheating <- use (uiState . uiCheatMode) curWinCondition <- use (gameState . winCondition)- announcementsSeq <- use (gameState . announcementQueue)+ announcementsSeq <- use (gameState . messageInfo . announcementQueue) let announcementsList = toList announcementsSeq -- Decide whether we need to update the current goal text and pop@@ -962,7 +955,7 @@ -- This clears the "flag" that indicate that the goals dialog needs to be -- automatically popped up.- gameState . announcementQueue .= mempty+ gameState . messageInfo . announcementQueue .= mempty hideGoals <- use $ uiState . uiHideGoals unless hideGoals $@@ -980,22 +973,20 @@ -- REPL events ------------------------------------------------------------ --- | Set the REPLForm to the given value, resetting type error checks to Nothing--- and removing uiError.+-- | Set the REPL to the given text and REPL prompt type. resetREPL :: T.Text -> REPLPrompt -> UIState -> UIState resetREPL t r ui = ui & uiREPL . replPromptText .~ t & uiREPL . replPromptType .~ r- & uiError .~ Nothing -- | Handle a user input event for the REPL. handleREPLEvent :: BrickEvent Name AppEvent -> EventM Name AppState () handleREPLEvent x = do s <- get- let repl = s ^. uiState . uiREPL- controlMode = repl ^. replControlMode- uinput = repl ^. replPromptText+ let theRepl = s ^. uiState . uiREPL+ controlMode = theRepl ^. replControlMode+ uinput = theRepl ^. replPromptText case x of -- Handle Ctrl-c here so we can always cancel the currently running -- base program no matter what REPL control mode we are in.@@ -1013,9 +1004,12 @@ _ -> if T.null uinput then uiState . uiREPL . replControlMode .= Piloting- else uiState . uiError ?= "Please clear the REPL first."+ else do+ let err = REPLError "Please clear the REPL before engaging pilot mode."+ uiState . uiREPL . replHistory %= addREPLItem err+ invalidateCacheEntry REPLHistoryCache MetaChar 'k' -> do- when (isJust (s ^. gameState . inputHandler)) $ do+ when (isJust (s ^. gameState . gameControls . inputHandler)) $ do curMode <- use $ uiState . uiREPL . replControlMode (uiState . uiREPL . replControlMode) .= case curMode of Handling -> Typing; _ -> Handling @@ -1033,7 +1027,7 @@ -- | Run the installed input handler on a key combo entered by the user. runInputHandler :: KeyCombo -> EventM Name AppState () runInputHandler kc = do- mhandler <- use $ gameState . inputHandler+ mhandler <- use $ gameState . gameControls . inputHandler case mhandler of -- Shouldn't be possible to get here if there is no input handler, but -- if we do somehow, just do nothing.@@ -1042,7 +1036,7 @@ -- Make sure the base is currently idle; if so, apply the -- installed input handler function to a `key` value -- representing the typed input.- working <- use $ gameState . replWorking+ working <- use $ gameState . gameControls . replWorking unless working $ do s <- get let topCtx = topContext s@@ -1077,8 +1071,8 @@ modify validateREPLForm handleREPLEventTyping $ Key V.KEnter - setCmd nt repl =- repl+ setCmd nt theRepl =+ theRepl & replPromptText .~ nt & replPromptType .~ CmdPrompt [] @@ -1086,19 +1080,19 @@ runBaseWebCode uinput = do s <- get let topCtx = topContext s- unless (s ^. gameState . replWorking) $+ unless (s ^. gameState . gameControls . replWorking) $ runBaseCode topCtx uinput runBaseCode :: (MonadState AppState m) => RobotContext -> T.Text -> m ()-runBaseCode topCtx uinput =+runBaseCode topCtx uinput = do+ uiState . uiREPL . replHistory %= addREPLItem (REPLEntry uinput)+ uiState %= resetREPL "" (CmdPrompt []) case processTerm' (topCtx ^. defTypes) (topCtx ^. defReqs) uinput of Right mt -> do- uiState %= resetREPL "" (CmdPrompt [])- uiState . uiREPL . replHistory %= addREPLItem (REPLEntry uinput) uiState . uiREPL . replHistory . replHasExecutedManualInput .= True runBaseTerm topCtx mt Left err -> do- uiState . uiError ?= err+ uiState . uiREPL . replHistory %= addREPLItem (REPLError err) runBaseTerm :: (MonadState AppState m) => RobotContext -> Maybe ProcessedTerm -> m () runBaseTerm topCtx =@@ -1109,7 +1103,7 @@ -- 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))+ (gameState . gameControls . 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@@ -1131,57 +1125,66 @@ -- | Handle a user input event for the REPL. handleREPLEventTyping :: BrickEvent Name AppEvent -> EventM Name AppState () handleREPLEventTyping = \case- Key V.KEnter -> do- s <- get- let topCtx = topContext s- repl = s ^. uiState . uiREPL- uinput = repl ^. replPromptText+ -- Scroll the REPL on PageUp or PageDown+ Key V.KPageUp -> vScrollPage replScroll Brick.Up+ Key V.KPageDown -> vScrollPage replScroll Brick.Down+ k -> do+ -- On any other key event, jump to the bottom of the REPL then handle the event+ vScrollToEnd replScroll+ case k of+ Key V.KEnter -> do+ s <- get+ let topCtx = topContext s+ theRepl = s ^. uiState . uiREPL+ uinput = theRepl ^. replPromptText - if not $ s ^. gameState . replWorking- then case repl ^. replPromptType of- CmdPrompt _ -> runBaseCode topCtx uinput- SearchPrompt hist ->- case lastEntry uinput hist of- Nothing -> uiState %= resetREPL "" (CmdPrompt [])- Just found- | T.null uinput -> uiState %= resetREPL "" (CmdPrompt [])- | otherwise -> do- uiState %= resetREPL found (CmdPrompt [])- modify validateREPLForm- else continueWithoutRedraw- Key V.KUp -> modify $ adjReplHistIndex Older- Key V.KDown -> modify $ adjReplHistIndex Newer- ControlChar 'r' -> do- s <- get- let uinput = s ^. uiState . uiREPL . replPromptText- case s ^. uiState . uiREPL . replPromptType of- CmdPrompt _ -> uiState . uiREPL . replPromptType .= SearchPrompt (s ^. uiState . uiREPL . replHistory)- SearchPrompt rh -> case lastEntry uinput rh of- Nothing -> pure ()- Just found -> uiState . uiREPL . replPromptType .= SearchPrompt (removeEntry found rh)- CharKey '\t' -> do- s <- get- let names = s ^.. gameState . baseRobot . robotContext . defTypes . to assocs . traverse . _1- uiState . uiREPL %= tabComplete names (s ^. gameState . entityMap)- modify validateREPLForm- EscapeKey -> do- formSt <- use $ uiState . uiREPL . replPromptType- case formSt of- CmdPrompt {} -> continueWithoutRedraw- SearchPrompt _ ->- uiState %= resetREPL "" (CmdPrompt [])- ControlChar 'd' -> do- text <- use $ uiState . uiREPL . replPromptText- if text == T.empty- then toggleModal QuitModal- else continueWithoutRedraw- -- finally if none match pass the event to the editor- ev -> do- Brick.zoom (uiState . uiREPL . replPromptEditor) (handleEditorEvent ev)- uiState . uiREPL . replPromptType %= \case- CmdPrompt _ -> CmdPrompt [] -- reset completions on any event passed to editor- SearchPrompt a -> SearchPrompt a- modify validateREPLForm+ if not $ s ^. gameState . gameControls . replWorking+ then case theRepl ^. replPromptType of+ CmdPrompt _ -> do+ runBaseCode topCtx uinput+ invalidateCacheEntry REPLHistoryCache+ SearchPrompt hist ->+ case lastEntry uinput hist of+ Nothing -> uiState %= resetREPL "" (CmdPrompt [])+ Just found+ | T.null uinput -> uiState %= resetREPL "" (CmdPrompt [])+ | otherwise -> do+ uiState %= resetREPL found (CmdPrompt [])+ modify validateREPLForm+ else continueWithoutRedraw+ Key V.KUp -> modify $ adjReplHistIndex Older+ Key V.KDown -> modify $ adjReplHistIndex Newer+ ControlChar 'r' -> do+ s <- get+ let uinput = s ^. uiState . uiREPL . replPromptText+ case s ^. uiState . uiREPL . replPromptType of+ CmdPrompt _ -> uiState . uiREPL . replPromptType .= SearchPrompt (s ^. uiState . uiREPL . replHistory)+ SearchPrompt rh -> case lastEntry uinput rh of+ Nothing -> pure ()+ Just found -> uiState . uiREPL . replPromptType .= SearchPrompt (removeEntry found rh)+ CharKey '\t' -> do+ s <- get+ let names = s ^.. gameState . baseRobot . robotContext . defTypes . to assocs . traverse . _1+ uiState . uiREPL %= tabComplete names (s ^. gameState . landscape . entityMap)+ modify validateREPLForm+ EscapeKey -> do+ formSt <- use $ uiState . uiREPL . replPromptType+ case formSt of+ CmdPrompt {} -> continueWithoutRedraw+ SearchPrompt _ ->+ uiState %= resetREPL "" (CmdPrompt [])+ ControlChar 'd' -> do+ text <- use $ uiState . uiREPL . replPromptText+ if text == T.empty+ then toggleModal QuitModal+ else continueWithoutRedraw+ -- finally if none match pass the event to the editor+ ev -> do+ Brick.zoom (uiState . uiREPL . replPromptEditor) (handleEditorEvent ev)+ uiState . uiREPL . replPromptType %= \case+ CmdPrompt _ -> CmdPrompt [] -- reset completions on any event passed to editor+ SearchPrompt a -> SearchPrompt a+ modify validateREPLForm data CompletionType = FunctionName@@ -1192,8 +1195,8 @@ -- reserved words and names in scope (in the case of function names) or -- entity names (in the case of string literals). tabComplete :: [Var] -> EntityMap -> REPLState -> REPLState-tabComplete names em repl = case repl ^. replPromptType of- SearchPrompt _ -> repl+tabComplete names em theRepl = case theRepl ^. replPromptType of+ SearchPrompt _ -> theRepl CmdPrompt mms -- Case 1: If completion candidates have already been -- populated via case (3), cycle through them.@@ -1241,9 +1244,9 @@ entityNames = M.keys $ entitiesByName em - t = repl ^. replPromptText+ t = theRepl ^. replPromptText setCmd nt ms =- repl+ theRepl & replPromptText .~ nt & replPromptType .~ CmdPrompt ms @@ -1254,7 +1257,7 @@ case replPrompt of CmdPrompt _ | T.null uinput ->- let theType = s ^. gameState . replStatus . replActiveType+ let theType = s ^. gameState . gameControls . replStatus . replActiveType in s & uiState . uiREPL . replType .~ theType CmdPrompt _ | otherwise ->@@ -1279,20 +1282,20 @@ & validateREPLForm where moveREPL :: REPLState -> REPLState- moveREPL repl =+ moveREPL theRepl = newREPL- & (if replIndexIsAtInput (repl ^. replHistory) then saveLastEntry else id)+ & (if replIndexIsAtInput (theRepl ^. replHistory) then saveLastEntry else id) & (if oldEntry /= newEntry then showNewEntry else id) where -- new AppState after moving the repl index newREPL :: REPLState- newREPL = repl & replHistory %~ moveReplHistIndex d oldEntry+ newREPL = theRepl & replHistory %~ moveReplHistIndex d oldEntry - saveLastEntry = replLast .~ (repl ^. replPromptText)+ saveLastEntry = replLast .~ (theRepl ^. replPromptText) showNewEntry = (replPromptEditor .~ newREPLEditor newEntry) . (replPromptType .~ CmdPrompt []) -- get REPL data- getCurrEntry = fromMaybe (repl ^. replLast) . getCurrentItemText . view replHistory- oldEntry = getCurrEntry repl+ getCurrEntry = fromMaybe (theRepl ^. replLast) . getCurrentItemText . view replHistory+ oldEntry = getCurrEntry theRepl newEntry = getCurrEntry newREPL ------------------------------------------------------------@@ -1313,7 +1316,7 @@ Key k | k `elem` moveKeys -> do c <- use $ gameState . creativeMode- s <- use $ gameState . worldScrollable+ s <- use $ gameState . landscape . worldScrollable when (c || s) $ scrollView (.+^ (worldScrollDist *^ keyToDir k)) CharKey 'c' -> do invalidateCacheEntry WorldCache@@ -1445,7 +1448,7 @@ case isActive <$> (s ^? gameState . baseRobot) of Just False -> do- gameState . replStatus .= REPLWorking (Typed Nothing PolyUnit (R.singletonCap CMake))+ gameState . gameControls . 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
@@ -53,9 +53,9 @@ where -- Set the game to AutoPause if needed ensurePause = do- pause <- use $ gameState . paused+ pause <- use $ gameState . temporal . paused unless (pause || isRunningModal mt) $ do- gameState . runStatus .= AutoPause+ gameState . temporal . runStatus .= AutoPause -- | The running modals do not autopause the game. isRunningModal :: ModalType -> Bool@@ -79,8 +79,8 @@ mext <- lookupExtent WorldExtent forM_ mext $ \(Extent _ _ size) -> do gs <- use gameState- let vr = viewingRegion gs (over both fromIntegral size)- gameState . multiWorld %= M.adjust (W.loadRegion (vr ^. planar)) (vr ^. subworld)+ let vr = viewingRegion (gs ^. viewCenter) (over both fromIntegral size)+ gameState . landscape . multiWorld %= M.adjust (W.loadRegion (vr ^. planar)) (vr ^. subworld) mouseLocToWorldCoords :: Brick.Location -> EventM Name GameState (Maybe (Cosmic W.Coords)) mouseLocToWorldCoords (Brick.Location mouseLoc) = do@@ -88,7 +88,7 @@ case mext of Nothing -> pure Nothing Just ext -> do- region <- gets $ flip viewingRegion (bimap fromIntegral fromIntegral (extentSize ext))+ region <- gets $ flip viewingRegion (bimap fromIntegral fromIntegral (extentSize ext)) . view viewCenter let regionStart = W.unCoords (fst $ region ^. planar) mouseLoc' = bimap fromIntegral fromIntegral mouseLoc mx = snd mouseLoc' + fst regionStart
src/Swarm/TUI/Editor/Controller.hs view
@@ -1,5 +1,7 @@ {-# LANGUAGE OverloadedStrings #-} +-- |+-- SPDX-License-Identifier: BSD-3-Clause module Swarm.TUI.Editor.Controller where import Brick hiding (Direction (..), Location (..))@@ -25,6 +27,7 @@ import Swarm.TUI.Model import Swarm.TUI.Model.Name import Swarm.TUI.Model.UI+import Swarm.Util (hoistMaybe) import Swarm.Util.Erasable (maybeToErasable) import System.Clock @@ -51,14 +54,13 @@ handleCtrlLeftClick mouseLoc = do worldEditor <- use $ uiState . uiWorldEditor _ <- runMaybeT $ do- guard $ worldEditor ^. isWorldEditorEnabled+ guard $ worldEditor ^. worldOverdraw . isWorldEditorEnabled let getSelected x = snd <$> BL.listSelectedElement x maybeTerrainType = getSelected $ worldEditor ^. terrainList maybeEntityPaint = getSelected $ worldEditor ^. entityPaintList- -- TODO (#1151): Use hoistMaybe when available- terrain <- MaybeT . pure $ maybeTerrainType+ terrain <- hoistMaybe maybeTerrainType mouseCoords <- MaybeT $ Brick.zoom gameState $ mouseLocToWorldCoords mouseLoc- uiState . uiWorldEditor . paintedTerrain %= M.insert (mouseCoords ^. planar) (terrain, maybeToErasable maybeEntityPaint)+ uiState . uiWorldEditor . worldOverdraw . paintedTerrain %= M.insert (mouseCoords ^. planar) (terrain, maybeToErasable maybeEntityPaint) uiState . uiWorldEditor . lastWorldEditorMessage .= Nothing immediatelyRedrawWorld return ()@@ -67,9 +69,9 @@ handleRightClick mouseLoc = do worldEditor <- use $ uiState . uiWorldEditor _ <- runMaybeT $ do- guard $ worldEditor ^. isWorldEditorEnabled+ guard $ worldEditor ^. worldOverdraw . isWorldEditorEnabled mouseCoords <- MaybeT $ Brick.zoom gameState $ mouseLocToWorldCoords mouseLoc- uiState . uiWorldEditor . paintedTerrain %= M.delete (mouseCoords ^. planar)+ uiState . uiWorldEditor . worldOverdraw . paintedTerrain %= M.delete (mouseCoords ^. planar) immediatelyRedrawWorld return () @@ -77,12 +79,12 @@ handleMiddleClick :: B.Location -> EventM Name AppState () handleMiddleClick mouseLoc = do worldEditor <- use $ uiState . uiWorldEditor- when (worldEditor ^. isWorldEditorEnabled) $ do- w <- use $ gameState . multiWorld+ when (worldEditor ^. worldOverdraw . isWorldEditorEnabled) $ do+ w <- use $ gameState . landscape . multiWorld let setTerrainPaint coords = do let (terrain, maybeElementPaint) =- EU.getContentAt- worldEditor+ EU.getEditorContentAt+ (worldEditor ^. worldOverdraw) w coords uiState . uiWorldEditor . terrainList %= BL.listMoveToElement terrain@@ -139,8 +141,8 @@ saveMapFile = do worldEditor <- use $ uiState . uiWorldEditor maybeBounds <- use $ uiState . uiWorldEditor . editingBounds . boundsRect- w <- use $ gameState . multiWorld- let mapCellGrid = EU.getEditedMapRectangle worldEditor maybeBounds w+ w <- use $ gameState . landscape . multiWorld+ let mapCellGrid = EU.getEditedMapRectangle (worldEditor ^. worldOverdraw) maybeBounds w let fp = worldEditor ^. outputFilePath maybeScenarioPair <- use $ uiState . scenarioRef
src/Swarm/TUI/Editor/Json.hs view
@@ -1,3 +1,5 @@+-- |+-- SPDX-License-Identifier: BSD-3-Clause module Swarm.TUI.Editor.Json where import Data.Text (Text)@@ -5,11 +7,13 @@ import GHC.Generics (Generic) import Swarm.Game.Entity (Entity) import Swarm.Game.Scenario.Topography.WorldDescription+import Swarm.Language.Syntax (Syntax)+import Swarm.Language.Text.Markdown (Document) data SkeletonScenario = SkeletonScenario { version :: Int , name :: Text- , description :: Text+ , description :: Document Syntax , creative :: Bool , entities :: [Entity] , world :: WorldDescriptionPaint
src/Swarm/TUI/Editor/Masking.hs view
@@ -1,3 +1,5 @@+-- |+-- SPDX-License-Identifier: BSD-3-Clause module Swarm.TUI.Editor.Masking where import Control.Lens hiding (Const, from)
src/Swarm/TUI/Editor/Model.hs view
@@ -1,6 +1,8 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TemplateHaskell #-} +-- |+-- SPDX-License-Identifier: BSD-3-Clause module Swarm.TUI.Editor.Model where import Brick.Focus@@ -39,7 +41,7 @@ Facade f -> f Ref e -> mkFacade e -getEntityName :: EntityFacade -> EntityName+getEntityName :: EntityFacade -> E.EntityName getEntityName (EntityFacade name _) = name data MapEditingBounds = MapEditingBounds@@ -52,13 +54,19 @@ makeLenses ''MapEditingBounds -data WorldEditor n = WorldEditor+data WorldOverdraw = WorldOverdraw { _isWorldEditorEnabled :: Bool- , _terrainList :: BL.List n TerrainType- , _entityPaintList :: BL.List n EntityFacade -- ^ This field has deferred initialization; it gets populated when a game -- is initialized. , _paintedTerrain :: M.Map W.Coords (TerrainWith EntityFacade)+ }++makeLenses ''WorldOverdraw++data WorldEditor n = WorldEditor+ { _worldOverdraw :: WorldOverdraw+ , _terrainList :: BL.List n TerrainType+ , _entityPaintList :: BL.List n EntityFacade , _editingBounds :: MapEditingBounds , _editorFocusRing :: FocusRing n , _outputFilePath :: FilePath@@ -70,10 +78,9 @@ initialWorldEditor :: TimeSpec -> WorldEditor Name initialWorldEditor ts = WorldEditor- False+ (WorldOverdraw False mempty) (BL.list TerrainList (V.fromList listEnums) 1) (BL.list EntityPaintList (V.fromList []) 1)- mempty bounds (focusRing $ map WorldEditorPanelControl listEnums) "mymap.yaml"
src/Swarm/TUI/Editor/Palette.hs view
@@ -19,7 +19,7 @@ import Data.Text qualified as T import Data.Tuple (swap) import Swarm.Game.Display (Display, defaultChar)-import Swarm.Game.Entity (entitiesByName)+import Swarm.Game.Entity (EntityName, entitiesByName) import Swarm.Game.Location import Swarm.Game.Scenario import Swarm.Game.Scenario.Topography.Area (AreaDimensions (..), getAreaDimensions)@@ -29,6 +29,7 @@ import Swarm.Game.Scenario.Topography.WorldPalette import Swarm.Game.Terrain (TerrainType, getTerrainDefaultPaletteChar) import Swarm.Game.Universe+import Swarm.Language.Text.Markdown (fromText) import Swarm.TUI.Editor.Json (SkeletonScenario (SkeletonScenario)) import Swarm.Util (binTuples, histogram) import Swarm.Util qualified as U@@ -116,7 +117,7 @@ SkeletonScenario (maybe 1 (^. scenarioVersion) maybeOriginalScenario) (maybe "My Scenario" (^. scenarioName) maybeOriginalScenario)- (maybe "The scenario description..." (^. scenarioDescription) maybeOriginalScenario)+ (maybe (fromText "The scenario description...") (^. scenarioDescription) maybeOriginalScenario) -- (maybe True (^. scenarioCreative) maybeOriginalScenario) True (M.elems $ entitiesByName customEntities)
src/Swarm/TUI/Editor/Util.hs view
@@ -1,9 +1,10 @@+-- |+-- SPDX-License-Identifier: BSD-3-Clause module Swarm.TUI.Editor.Util where import Control.Applicative ((<|>)) import Control.Lens hiding (Const, from) import Control.Monad (guard)-import Data.Int (Int32) import Data.Map qualified as M import Data.Map qualified as Map import Data.Maybe qualified as Maybe@@ -17,7 +18,6 @@ import Swarm.Game.Universe import Swarm.Game.World qualified as W import Swarm.TUI.Editor.Model-import Swarm.TUI.Model import Swarm.Util.Erasable getEntitiesForList :: EntityMap -> V.Vector EntityFacade@@ -35,12 +35,18 @@ a = EA.getAreaDimensions $ area myWorld lowerRightLoc = EA.upperLeftToBottomRight a upperLeftLoc -getContentAt ::- WorldEditor Name ->+getContentAt :: W.MultiWorld Int e -> Cosmic W.Coords -> (TerrainType, Maybe e)+getContentAt w coords = (underlyingCellTerrain, underlyingCellEntity)+ where+ underlyingCellEntity = W.lookupCosmicEntity coords w+ underlyingCellTerrain = W.lookupCosmicTerrain coords w++getEditorContentAt ::+ WorldOverdraw -> W.MultiWorld Int Entity -> Cosmic W.Coords -> (TerrainType, Maybe EntityPaint)-getContentAt editor w coords =+getEditorContentAt editorOverdraw w coords = (terrainWithOverride, entityWithOverride) where terrainWithOverride = Maybe.fromMaybe underlyingCellTerrain $ do@@ -53,22 +59,21 @@ Facade <$> erasableToMaybe e maybePaintedCell = do- guard $ editor ^. isWorldEditorEnabled+ guard $ editorOverdraw ^. isWorldEditorEnabled Map.lookup (coords ^. planar) pm - pm = editor ^. paintedTerrain+ pm = editorOverdraw ^. paintedTerrain entityWithOverride = (Ref <$> underlyingCellEntity) <|> maybeEntityOverride- underlyingCellEntity = W.lookupCosmicEntity coords w- underlyingCellTerrain = W.lookupCosmicTerrain coords w+ (underlyingCellTerrain, underlyingCellEntity) = getContentAt w coords -getTerrainAt ::- WorldEditor Name ->+getEditorTerrainAt ::+ WorldOverdraw -> W.MultiWorld Int Entity -> Cosmic W.Coords -> TerrainType-getTerrainAt editor w coords =- fst $ getContentAt editor w coords+getEditorTerrainAt editor w coords =+ fst $ getEditorContentAt editor w coords isOutsideTopLeftCorner :: -- | top left corner coords@@ -98,25 +103,32 @@ isOutsideTopLeftCorner tl coord || isOutsideBottomRightCorner br coord getEditedMapRectangle ::- WorldEditor Name ->+ WorldOverdraw -> Maybe (Cosmic W.BoundsRectangle) -> W.MultiWorld Int Entity -> [[CellPaintDisplay]] getEditedMapRectangle _ Nothing _ = [] getEditedMapRectangle worldEditor (Just (Cosmic subworldName coords)) w =+ getMapRectangle toFacade getContent coords+ where+ getContent = getEditorContentAt worldEditor w . Cosmic subworldName++getMapRectangle ::+ (d -> e) ->+ (W.Coords -> (TerrainType, Maybe d)) ->+ W.BoundsRectangle ->+ [[PCell e]]+getMapRectangle paintTransform contentFunc coords = map renderRow [yTop .. yBottom] where (W.Coords (yTop, xLeft), W.Coords (yBottom, xRight)) = coords - getContent = getContentAt worldEditor w . Cosmic subworldName-- drawCell :: Int32 -> Int32 -> CellPaintDisplay- drawCell rowIndex colIndex =+ drawCell f rowIndex colIndex = Cell terrain- (toFacade <$> maybeToErasable erasableEntity)+ (f <$> maybeToErasable erasableEntity) [] where- (terrain, erasableEntity) = getContent $ W.Coords (rowIndex, colIndex)+ (terrain, erasableEntity) = contentFunc $ W.Coords (rowIndex, colIndex) - renderRow rowIndex = map (drawCell rowIndex) [xLeft .. xRight]+ renderRow rowIndex = map (drawCell paintTransform rowIndex) [xLeft .. xRight]
src/Swarm/TUI/Editor/View.hs view
@@ -1,3 +1,5 @@+-- |+-- SPDX-License-Identifier: BSD-3-Clause module Swarm.TUI.Editor.View where import Brick hiding (Direction)@@ -11,20 +13,20 @@ import Swarm.Game.Terrain (TerrainType) import Swarm.Game.Universe import Swarm.Game.World qualified as W-import Swarm.TUI.Attr import Swarm.TUI.Border import Swarm.TUI.Editor.Model import Swarm.TUI.Model import Swarm.TUI.Model.Name import Swarm.TUI.Model.UI import Swarm.TUI.Panel+import Swarm.TUI.View.Attribute.Attr import Swarm.TUI.View.CellDisplay (renderDisplay) import Swarm.TUI.View.Util qualified as VU import Swarm.Util (listEnums) drawWorldEditor :: FocusRing Name -> UIState -> Widget Name drawWorldEditor toplevelFocusRing uis =- if worldEditor ^. isWorldEditorEnabled+ if worldEditor ^. worldOverdraw . isWorldEditorEnabled then panel highlightAttr
src/Swarm/TUI/Launch/Controller.hs view
@@ -44,7 +44,7 @@ editingParams .= parsedParams updateFocusRing parsedParams --- | Split this out from the combined parameter-validation function+-- | This is split out from the combined parameter-validation function -- because validating the seed is cheap, and shouldn't have to pay -- the cost of re-parsing script code as the user types in the seed -- selection field.@@ -57,7 +57,7 @@ editingParams .= newParams updateFocusRing newParams --- | If the FileBrowser is in "search mode", then we allow+-- | If the 'FileBrowser' is in "search mode", then we allow -- more of the key events to pass through. Otherwise, -- we intercept things like "q" (for quit) and Space (so that -- we can restrict file selection to at most one).
src/Swarm/TUI/Launch/Prep.hs view
@@ -70,10 +70,10 @@ (Just 1) -- only allow a single line -- | Called before any particular scenario is selected, so we--- supply some "Nothing"s as defaults to the "ValidatedLaunchParams".+-- supply some 'Nothing's as defaults to the 'ValidatedLaunchParams'. initConfigPanel :: IO LaunchOptions initConfigPanel = do- -- NOTE: This is kind of pointless, because we must re-instantiate the FileBrowser+ -- NOTE: This is kind of pointless, because we must re-instantiate the 'FB.FileBrowser' -- when it is first displayed, anyway. fb <- FB.newFileBrowser@@ -105,13 +105,14 @@ -- set the file browser to initially open that script's directory. -- Then set the launch dialog to be displayed. ----- Note that the FileBrowser widget normally allows multiple selections ("marked" files).+-- Note that the 'FB.FileBrowser' widget normally allows multiple selections ("marked" files). -- However, there do not exist any public "setters" set the marked files, so we have -- some workarounds:--- * When the user marks the first file, we immediately close the FileBrowser widget.--- * We re-instantiate the FileBrowser from scratch every time it is opened, so that+--+-- * When the user marks the first file, we immediately close the 'FB.FileBrowser' widget.+-- * We re-instantiate the 'FB.FileBrowser' from scratch every time it is opened, so that -- it is not possible to mark more than one file.--- * The "marked file" is persisted outside of the FileBrowser state, and the+-- * The "marked file" is persisted outside of the 'FB.FileBrowser' state, and the -- "initial directory" is set upon instantiation from that external state. prepareLaunchDialog :: ScenarioInfoPair ->
src/Swarm/TUI/Launch/View.hs view
@@ -22,10 +22,10 @@ import Swarm.Game.Scenario (scenarioSeed) import Swarm.Game.Scenario.Status (ParameterizableLaunchParams (..)) import Swarm.Game.State (getRunCodePath)-import Swarm.TUI.Attr import Swarm.TUI.Launch.Model import Swarm.TUI.Launch.Prep import Swarm.TUI.Model.Name+import Swarm.TUI.View.Attribute.Attr import Swarm.TUI.View.Util (EllipsisSide (Beginning), withEllipsis) import Swarm.Util (brackets, parens)
src/Swarm/TUI/Model.hs view
@@ -78,6 +78,7 @@ populateInventoryList, infoScroll, modalScroll,+ replScroll, -- * Runtime state RuntimeState,@@ -89,8 +90,7 @@ stdEntityMap, stdRecipes, appData,- stdAdjList,- stdNameList,+ nameParts, -- ** Utility logEvent,@@ -126,15 +126,12 @@ import Control.Lens hiding (from, (<.>)) import Control.Monad ((>=>)) import Control.Monad.State (MonadState)-import Data.Array (Array, listArray) import Data.List (findIndex) import Data.List.NonEmpty (NonEmpty (..)) import Data.Map (Map)-import Data.Map qualified as M import Data.Maybe (fromMaybe) import Data.Sequence (Seq) import Data.Text (Text)-import Data.Text qualified as T (lines) import Data.Vector qualified as V import GitHash (GitInfo) import Graphics.Vty (ColorMode (..))@@ -143,13 +140,14 @@ import Swarm.Game.Entity as E import Swarm.Game.Failure import Swarm.Game.Recipe (Recipe, loadRecipes)-import Swarm.Game.ResourceLoading (readAppData)+import Swarm.Game.ResourceLoading (NameGenerator, initNameGenerator, readAppData) import Swarm.Game.Robot import Swarm.Game.Scenario.Status import Swarm.Game.ScenarioInfo (ScenarioCollection, loadScenarios, _SISingle) import Swarm.Game.State import Swarm.Game.World.Load (loadWorlds) import Swarm.Game.World.Typecheck (WorldMap)+import Swarm.Log import Swarm.TUI.Inventory.Sorting import Swarm.TUI.Model.Menu import Swarm.TUI.Model.Name@@ -157,9 +155,7 @@ import Swarm.TUI.Model.UI import Swarm.Util.Lens (makeLensesNoSigs) import Swarm.Version (NewReleaseFailure (NoMainUpstreamRelease))-import System.FilePath ((<.>)) import Text.Fuzzy qualified as Fuzzy-import Witch (into) ------------------------------------------------------------ -- Custom UI label types@@ -186,6 +182,9 @@ modalScroll :: ViewportScroll Name modalScroll = viewportScroll ModalViewport +replScroll :: ViewportScroll Name+replScroll = viewportScroll REPLViewport+ -- ---------------------------------------------------------------------------- -- Runtime state -- -- ----------------------------------------------------------------------------@@ -199,8 +198,7 @@ , _stdEntityMap :: EntityMap , _stdRecipes :: [Recipe Entity] , _appData :: Map Text Text- , _stdAdjList :: Array Int Text- , _stdNameList :: Array Int Text+ , _nameParts :: NameGenerator } initRuntimeState ::@@ -215,15 +213,7 @@ worlds <- loadWorlds entities scenarios <- loadScenarios entities worlds appDataMap <- readAppData-- let getDataLines f = case M.lookup f appDataMap of- Nothing ->- throwError $- AssetNotLoaded (Data NameGeneration) (into @FilePath f <.> ".txt") (DoesNotExist File)- Just content -> return . tail . T.lines $ content- adjs <- getDataLines "adjectives"- names <- getDataLines "names"-+ nameGen <- initNameGenerator appDataMap return $ RuntimeState { _webPort = Nothing@@ -234,8 +224,7 @@ , _stdEntityMap = entities , _stdRecipes = recipes , _appData = appDataMap- , _stdAdjList = listArray (0, length adjs - 1) adjs- , _stdNameList = listArray (0, length names - 1) names+ , _nameParts = nameGen } makeLensesNoSigs ''RuntimeState@@ -274,30 +263,26 @@ -- the logo, about page, tutorial story, etc. appData :: Lens' RuntimeState (Map Text Text) --- | List of words for use in building random robot names.-stdAdjList :: Lens' RuntimeState (Array Int Text)---- | List of words for use in building random robot names.-stdNameList :: Lens' RuntimeState (Array Int Text)+-- | Lists of words/adjectives for use in building random robot names.+nameParts :: Lens' RuntimeState NameGenerator -------------------------------------------------- -- Utility -- | Simply log to the runtime event log.-logEvent :: LogSource -> (Text, RID) -> Text -> Notifications LogEntry -> Notifications LogEntry-logEvent src (who, rid) msg el =+logEvent :: LogSource -> Severity -> Text -> Text -> Notifications LogEntry -> Notifications LogEntry+logEvent src sev who msg el = el & notificationsCount %~ succ & notificationsContent %~ (l :) where- l = LogEntry (TickNumber 0) src who rid Omnipresent msg+ l = LogEntry (TickNumber 0) src sev who msg -- | Create a 'GameStateConfig' record from the 'RuntimeState'. mkGameStateConfig :: RuntimeState -> GameStateConfig mkGameStateConfig rs = GameStateConfig- { initAdjList = rs ^. stdAdjList- , initNameList = rs ^. stdNameList+ { initNameParts = rs ^. nameParts , initEntities = rs ^. stdEntityMap , initRecipes = rs ^. stdRecipes , initWorldMap = rs ^. worlds@@ -477,6 +462,6 @@ where ctx = fromMaybe emptyRobotContext $ s ^? gameState . baseRobot . robotContext - ctxPossiblyWithIt = case s ^. gameState . replStatus of+ ctxPossiblyWithIt = case s ^. gameState . gameControls . replStatus of REPLDone (Just p) -> ctx & at "it" ?~ p _ -> ctx
src/Swarm/TUI/Model/Goal.hs view
@@ -41,9 +41,9 @@ Completed | -- | A goal that can no longer be achieved. -- If this goal is not an "optional" goal, then the player- -- also "Loses" the scenario.+ -- also "loses" the scenario. --- -- Note that currently the only way to "Fail" a goal is by way+ -- 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)@@ -69,7 +69,11 @@ deriving (Generic, ToJSON) instance ToSample GoalTracking where- toSamples _ = SD.noSamples+ toSamples _ =+ SD.samples+ [ GoalTracking mempty mempty+ -- TODO: #1552 add simple objective sample+ ] data GoalDisplay = GoalDisplay { _goalsContent :: GoalTracking
src/Swarm/TUI/Model/Menu.hs view
@@ -6,6 +6,9 @@ -- | -- SPDX-License-Identifier: BSD-3-Clause+--+-- Sum types that represent menu options,+-- modal dialogs, and buttons. module Swarm.TUI.Model.Menu where import Brick.Widgets.Dialog (Dialog)@@ -105,7 +108,7 @@ -- 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) []+mkNewGameMenu cheat sc path = fmap NewGameMenu $ NE.nonEmpty =<< go (Just sc) (splitPath path) [] where go :: Maybe ScenarioCollection ->
src/Swarm/TUI/Model/Name.hs view
@@ -1,5 +1,11 @@ -- | -- SPDX-License-Identifier: BSD-3-Clause+--+-- Sum types representing the Brick names+-- for every referenceable widget.+--+-- Nesting of name types is utilized often to simplify+-- case matching. module Swarm.TUI.Model.Name where data WorldEditorFocusable@@ -58,6 +64,8 @@ WorldEditorPanelControl WorldEditorFocusable | -- | The REPL input form. REPLInput+ | -- | The REPL history cache.+ REPLHistoryCache | -- | The render cache for the world view. WorldCache | -- | The cached extent for the world view.@@ -91,6 +99,8 @@ InfoViewport | -- | The scrollable viewport for any modal dialog. ModalViewport+ | -- | The scrollable viewport for the REPL.+ REPLViewport | -- | A clickable button in a modal dialog. Button Button deriving (Eq, Ord, Show, Read)
src/Swarm/TUI/Model/Repl.hs view
@@ -21,6 +21,7 @@ addREPLItem, restartREPLHistory, getLatestREPLHistoryItems,+ getSessionREPLHistoryItems, moveReplHistIndex, getCurrentItemText, replIndexIsAtInput,@@ -77,15 +78,24 @@ REPLEntry Text | -- | A response printed by the system. REPLOutput Text+ | -- | An error printed by the system.+ REPLError Text deriving (Eq, Ord, Show, Read) instance ToSample REPLHistItem where- toSamples _ = SD.noSamples+ toSamples _ =+ SD.samples+ [ REPLEntry "grab"+ , REPLOutput "it0 : text = \"tree\""+ , REPLEntry "place tree"+ , REPLError "1:7: Unbound variable tree"+ ] instance ToJSON REPLHistItem where toJSON e = case e of REPLEntry x -> object ["in" .= x] REPLOutput x -> object ["out" .= x]+ REPLError x -> object ["err" .= x] -- | Useful helper function to only get user input text. getREPLEntry :: REPLHistItem -> Maybe Text@@ -102,6 +112,7 @@ replItemText = \case REPLEntry t -> t REPLOutput t -> t+ REPLError 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.@@ -129,23 +140,24 @@ -- It will be set on load and reset on save (happens during exit). replStart :: Lens' REPLHistory Int --- | Note: Instead of adding a dedicated field to the REPLHistory record,--- an early attempt entailed checking for:+-- | Keep track of whether the user has explicitly executed commands+-- at the REPL prompt, thus making them ineligible for code size scoring. ----- _replIndex > _replStart+-- Note: Instead of adding a dedicated field to the 'REPLHistory' record,+-- an early attempt entailed checking for: ----- However, executing an initial script causes--- a "REPLOutput" to be appended to the REPL history,--- which increments the replIndex, and thus makes--- the Index greater than the Start even though--- the player has input not commands into the REPL.+-- @_replIndex > _replStart@ ----- Therefore, a dedicated boolean is introduced into--- REPLHistory which simply latches True when the user--- has input a command.+-- However, executing an initial script causes a "REPLOutput" to be+-- appended to the REPL history, which increments the replIndex, and+-- thus makes the Index greater than the Start even though the+-- player has not input commands directly into the REPL. ----- An alternative is described here:--- https://github.com/swarm-game/swarm/pull/974#discussion_r1112380380+-- Therefore, a dedicated boolean is introduced into 'REPLHistory'+-- which simply latches True when the user has input a command.+--+-- An alternative is described in+-- <https://github.com/swarm-game/swarm/pull/974#discussion_r1112380380 issue #974>. replHasExecutedManualInput :: Lens' REPLHistory Bool -- | Create new REPL history (i.e. from loaded history file lines).@@ -185,6 +197,11 @@ latestN = Seq.drop oldestIndex $ h ^. replSeq oldestIndex = max (h ^. replStart) $ length (h ^. replSeq) - n +-- | Get only the items from the REPL history that were entered during+-- the current session.+getSessionREPLHistoryItems :: REPLHistory -> Seq REPLHistItem+getSessionREPLHistoryItems h = Seq.drop (h ^. replStart) (h ^. replSeq)+ data TimeDir = Newer | Older deriving (Eq, Ord, Show) moveReplHistIndex :: TimeDir -> Text -> REPLHistory -> REPLHistory@@ -210,14 +227,14 @@ replIndexIsAtInput :: REPLHistory -> Bool replIndexIsAtInput repl = repl ^. replIndex == replLength repl --- | Given some text, removes the REPLEntry within REPLHistory which is equal to that.+-- | 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+-- | 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@@ -291,7 +308,7 @@ -- | 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+-- | Convenience 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@@ -299,7 +316,7 @@ g r = r ^. replPromptEditor . to getEditContents . to T.concat s r t = r & replPromptEditor .~ newREPLEditor t --- | Whether the prompt text is a valid 'Term'.+-- | Whether the prompt text is a valid 'Swarm.Language.Syntax.Term'. replValid :: Lens' REPLState Bool -- | The type of the current REPL input which should be displayed to
src/Swarm/TUI/Model/StateUpdate.hs view
@@ -43,7 +43,6 @@ import Swarm.Game.Achievement.Definitions import Swarm.Game.Achievement.Persistence import Swarm.Game.Failure (SystemFailure)-import Swarm.Game.Log (ErrorLevel (..), LogSource (ErrorTrace)) import Swarm.Game.Scenario (loadScenario, scenarioAttrs, scenarioWorlds) import Swarm.Game.Scenario.Scoring.Best import Swarm.Game.Scenario.Scoring.ConcreteMetrics@@ -58,7 +57,7 @@ ) import Swarm.Game.State import Swarm.Language.Pretty (prettyText)-import Swarm.TUI.Attr (swarmAttrMap)+import Swarm.Log (LogSource (SystemLog), Severity (..)) import Swarm.TUI.Editor.Model qualified as EM import Swarm.TUI.Editor.Util qualified as EU import Swarm.TUI.Inventory.Sorting@@ -67,7 +66,8 @@ import Swarm.TUI.Model.Goal (emptyGoalDisplay) import Swarm.TUI.Model.Repl import Swarm.TUI.Model.UI-import Swarm.TUI.View.CustomStyling (toAttrPair)+import Swarm.TUI.View.Attribute.Attr (swarmAttrMap)+import Swarm.TUI.View.Attribute.CustomStyling (toAttrPair) import Swarm.Util.Effect (asExceptT, withThrow) import System.Clock @@ -85,7 +85,7 @@ addWarnings :: RuntimeState -> [SystemFailure] -> RuntimeState addWarnings = List.foldl' logWarning where- logWarning rs' w = rs' & eventLog %~ logEvent (ErrorTrace Error) ("UI Loading", -8) (prettyText w)+ logWarning rs' w = rs' & eventLog %~ logEvent SystemLog Error "UI Loading" (prettyText w) -- | Based on the command line options, should we skip displaying the -- menu?@@ -123,7 +123,7 @@ case skipMenu opts of False -> return $ AppState gs (ui & lgTicksPerSecond .~ defaultInitLgTicksPerSecond) rs True -> do- (scenario, path) <- loadScenario (fromMaybe "classic" userScenario) (gs ^. entityMap) (rs ^. worlds)+ (scenario, path) <- loadScenario (fromMaybe "classic" userScenario) (gs ^. landscape . entityMap) (rs ^. worlds) maybeRunScript <- traverse parseCodeFile scriptToRun let maybeAutoplay = do@@ -258,7 +258,7 @@ & uiWorldEditor . EM.entityPaintList %~ BL.listReplace entityList Nothing & uiWorldEditor . EM.editingBounds . EM.boundsRect %~ setNewBounds where- entityList = EU.getEntitiesForList $ gs ^. entityMap+ entityList = EU.getEntitiesForList $ gs ^. landscape . entityMap (isEmptyArea, newBounds) = EU.getEditingBounds $ NE.head $ scenario ^. scenarioWorlds setNewBounds maybeOldBounds =
src/Swarm/TUI/Model/UI.hs view
@@ -20,10 +20,7 @@ uiInventory, uiInventorySort, uiInventorySearch,- uiMoreInfoTop,- uiMoreInfoBot, uiScrollToEnd,- uiError, uiModal, uiGoal, uiHideGoals,@@ -76,7 +73,6 @@ ) import Swarm.Game.Universe import Swarm.Game.World qualified as W-import Swarm.TUI.Attr (swarmAttrMap) import Swarm.TUI.Editor.Model import Swarm.TUI.Inventory.Sorting import Swarm.TUI.Launch.Model@@ -85,6 +81,7 @@ import Swarm.TUI.Model.Menu import Swarm.TUI.Model.Name import Swarm.TUI.Model.Repl+import Swarm.TUI.View.Attribute.Attr (swarmAttrMap) import Swarm.Util import Swarm.Util.Lens (makeLensesExcluding) import System.Clock@@ -107,10 +104,7 @@ , _uiInventory :: Maybe (Int, BL.List Name InventoryListEntry) , _uiInventorySort :: InventorySortOptions , _uiInventorySearch :: Maybe Text- , _uiMoreInfoTop :: Bool- , _uiMoreInfoBot :: Bool , _uiScrollToEnd :: Bool- , _uiError :: Maybe Text , _uiModal :: Maybe Modal , _uiGoal :: GoalDisplay , _uiHideGoals :: Bool@@ -142,8 +136,11 @@ -- | 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+-- | 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 @@ -154,7 +151,7 @@ uiLaunchConfig :: Lens' UIState LaunchOptions -- | The focus ring is the set of UI panels we can cycle among using--- the Tab key.+-- the @Tab@ key. uiFocusRing :: Lens' UIState (FocusRing Name) -- | The last clicked position on the world view.@@ -177,21 +174,11 @@ -- 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+-- | 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) @@ -199,21 +186,21 @@ -- has been displayed to the user initially. uiGoal :: Lens' UIState GoalDisplay --- | When running with --autoplay, suppress the goal dialogs.+-- | When running with @--autoplay@, suppress the goal dialogs. ----- For developement, the --cheat flag shows goals again.+-- For development, the @--cheat@ flag shows goals again. uiHideGoals :: Lens' UIState Bool -- | Map of achievements that were attained uiAchievements :: Lens' UIState (Map CategorizedAchievement Attainment) --- | A toggle to show the FPS by pressing `f`+-- | A toggle to show the FPS by pressing @f@ uiShowFPS :: Lens' UIState Bool --- | A toggle to expand or collapse the REPL by pressing `Ctrl-k`+-- | A toggle to expand or collapse the REPL by pressing @Ctrl-k@ uiShowREPL :: Lens' UIState Bool --- | A toggle to show or hide inventory items with count 0 by pressing `0`+-- | A toggle to show or hide inventory items with count 0 by pressing @0@ uiShowZero :: Lens' UIState Bool -- | A toggle to show debug.@@ -231,10 +218,10 @@ -- | Whether the Inventory ui panel should update uiInventoryShouldUpdate :: Lens' UIState Bool --- | Computed ticks per milli seconds+-- | Computed ticks per milliseconds uiTPF :: Lens' UIState Double --- | Computed frames per milli seconds+-- | Computed frames per milliseconds uiFPS :: Lens' UIState Double -- | Attribute map@@ -272,10 +259,10 @@ -- | The time of the last info widget update lastInfoTime :: Lens' UIState TimeSpec --- | The time of the last 'Frame' event.+-- | The time of the last 'Swarm.TUI.Model.Frame' event. lastFrameTime :: Lens' UIState TimeSpec --- | The amount of accumulated real time. Every time we get a 'Frame'+-- | The amount of accumulated real time. Every time we get a 'Swarm.TUI.Model.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.@@ -329,10 +316,7 @@ , _uiInventory = Nothing , _uiInventorySort = defaultSortOptions , _uiInventorySearch = Nothing- , _uiMoreInfoTop = False- , _uiMoreInfoBot = False , _uiScrollToEnd = False- , _uiError = Nothing , _uiModal = Nothing , _uiGoal = emptyGoalDisplay , _uiHideGoals = False
src/Swarm/TUI/View.hs view
@@ -18,12 +18,13 @@ drawKeyCmd, -- * World- drawWorld,+ drawWorldPane, -- * Robot panel drawRobotPanel, drawItem, drawLabelledEntityName,+ renderDutyCycle, -- * Info panel drawInfoPanel,@@ -51,6 +52,7 @@ import Control.Monad (guard) import Data.Array (range) import Data.Bits (shiftL, shiftR, (.&.))+import Data.Foldable (toList) import Data.Foldable qualified as F import Data.Functor (($>)) import Data.IntMap qualified as IM@@ -60,7 +62,7 @@ import Data.List.NonEmpty qualified as NE import Data.List.Split (chunksOf) import Data.Map qualified as M-import Data.Maybe (catMaybes, fromMaybe, isJust, mapMaybe, maybeToList)+import Data.Maybe (catMaybes, fromMaybe, isJust, listToMaybe, mapMaybe, maybeToList) import Data.Semigroup (sconcat) import Data.Sequence qualified as Seq import Data.Set qualified as Set (toList)@@ -69,14 +71,23 @@ import Data.Time (NominalDiffTime, defaultTimeLocale, formatTime) import Linear import Network.Wai.Handler.Warp (Port)+import Numeric (showFFloat) import Swarm.Constant-import Swarm.Game.CESK (CESK (..), TickNumber (..))+import Swarm.Game.CESK (CESK (..), TickNumber (..), addTicks) import Swarm.Game.Display import Swarm.Game.Entity as E import Swarm.Game.Location import Swarm.Game.Recipe import Swarm.Game.Robot-import Swarm.Game.Scenario (scenarioAuthor, scenarioDescription, scenarioName, scenarioObjectives)+import Swarm.Game.Scenario (+ scenarioAuthor,+ scenarioCreative,+ scenarioDescription,+ scenarioKnown,+ scenarioName,+ scenarioObjectives,+ scenarioSeed,+ ) import Swarm.Game.Scenario.Scoring.Best import Swarm.Game.Scenario.Scoring.CodeSize import Swarm.Game.Scenario.Scoring.ConcreteMetrics@@ -90,10 +101,10 @@ import Swarm.Game.Universe import Swarm.Game.World qualified as W import Swarm.Language.Capability (Capability (..), constCaps)-import Swarm.Language.Pretty (prettyText)+import Swarm.Language.Pretty (prettyText, prettyTextLine) import Swarm.Language.Syntax import Swarm.Language.Typecheck (inferConst)-import Swarm.TUI.Attr+import Swarm.Log import Swarm.TUI.Border import Swarm.TUI.Controller (ticksPerFrameCap) import Swarm.TUI.Editor.Model@@ -103,19 +114,22 @@ import Swarm.TUI.Launch.View import Swarm.TUI.Model import Swarm.TUI.Model.Goal (goalsContent, hasAnythingToShow)-import Swarm.TUI.Model.Repl (lastEntry)+import Swarm.TUI.Model.Repl (getSessionREPLHistoryItems, lastEntry) import Swarm.TUI.Model.UI import Swarm.TUI.Panel import Swarm.TUI.View.Achievement+import Swarm.TUI.View.Attribute.Attr import Swarm.TUI.View.CellDisplay import Swarm.TUI.View.Objective qualified as GR import Swarm.TUI.View.Util as VU import Swarm.Util+import Swarm.Util.UnitInterval+import Swarm.Util.WindowedCounter qualified as WC import Swarm.Version (NewReleaseFailure (..)) import System.Clock (TimeSpec (..)) import Text.Printf import Text.Wrap-import Witch (from, into)+import Witch (into) -- | The main entry point for drawing the entire UI. Figures out -- which menu screen we should show (if any), or just the game itself.@@ -233,10 +247,36 @@ drawDescription (SICollection _ _) = txtWrap " " drawDescription (SISingle (s, si)) = vBox- [ txtWrap (nonBlank (s ^. scenarioDescription))+ [ drawMarkdown (nonBlank (s ^. scenarioDescription))+ , hCenter . padTop (Pad 1) . vLimit 6 $ hLimitPercent 60 worldPeek , padTop (Pad 1) table ] where+ defaultVC = Cosmic DefaultRootSubworld origin++ -- The first robot is guaranteed to be the base.+ baseRobotLoc :: Maybe (Cosmic Location)+ baseRobotLoc = do+ theBaseRobot <- listToMaybe theRobots+ view trobotLocation theBaseRobot++ vc = fromMaybe defaultVC baseRobotLoc++ worldTuples = buildWorldTuples s+ theWorlds = genMultiWorld worldTuples $ fromMaybe 0 $ s ^. scenarioSeed+ theRobots = genRobotTemplates s worldTuples++ ri =+ RenderingInput theWorlds $+ getEntityIsKnown $+ EntityKnowledgeDependencies+ { isCreativeMode = s ^. scenarioCreative+ , globallyKnownEntities = s ^. scenarioKnown+ , theFocusedRobot = Nothing+ }+ renderCoord = renderDisplay . displayLocRaw (WorldOverdraw False mempty) ri []+ worldPeek = worldWidget renderCoord vc+ firstRow = ( withAttr dimAttr $ txt "Author:" , withAttr dimAttr . txt <$> s ^. scenarioAuthor@@ -401,12 +441,7 @@ highlightAttr fr (FocusablePanel InfoPanel)- ( plainBorder- & topLabels . centerLabel- .~ (if moreTop then Just (txt " · · · ") else Nothing)- & bottomLabels . centerLabel- .~ (if moreBot then Just (txt " · · · ") else Nothing)- )+ plainBorder $ drawInfoPanel s , hCenter . clickable (FocusablePanel WorldEditorPanel)@@ -421,13 +456,11 @@ where widg = case s ^. uiState . uiWorldCursor of Nothing -> str $ renderCoordsString $ s ^. gameState . viewCenter- Just coord -> clickable WorldPositionIndicator $ drawWorldCursorInfo (s ^. uiState . uiWorldEditor) (s ^. gameState) coord+ Just coord -> clickable WorldPositionIndicator $ drawWorldCursorInfo (s ^. uiState . uiWorldEditor . worldOverdraw) (s ^. gameState) coord -- Add clock display in top right of the world view if focused robot -- has a clock equipped addClock = topLabels . rightLabel ?~ padLeftRight 1 (drawClockDisplay (s ^. uiState . lgTicksPerSecond) $ s ^. gameState) fr = s ^. uiState . uiFocusRing- moreTop = s ^. uiState . uiMoreInfoTop- moreBot = s ^. uiState . uiMoreInfoBot showREPL = s ^. uiState . uiShowREPL rightPanel = if showREPL then worldPanel ++ replPanel else worldPanel ++ minimizedREPL minimizedREPL = case focusGetCurrent fr of@@ -444,7 +477,7 @@ & addCursorPos & addClock )- (drawWorld (s ^. uiState) (s ^. gameState))+ (drawWorldPane (s ^. uiState) (s ^. gameState)) , drawKeyMenu s ] replPanel =@@ -458,7 +491,7 @@ ) ( vLimit replHeight . padBottom Max- . padLeftRight 1+ . padLeft (Pad 1) $ drawREPL s ) ]@@ -471,7 +504,7 @@ DefaultRootSubworld -> [] SubworldName swName -> ["in", T.unpack swName] -drawWorldCursorInfo :: WorldEditor Name -> GameState -> Cosmic W.Coords -> Widget Name+drawWorldCursorInfo :: WorldOverdraw -> GameState -> Cosmic W.Coords -> Widget Name drawWorldCursorInfo worldEditor g cCoords = case getStatic g coords of Just s -> renderDisplay $ displayStatic s@@ -490,8 +523,9 @@ where f cell preposition = [renderDisplay cell, txt preposition] - terrain = displayTerrainCell worldEditor g cCoords- entity = displayEntityCell worldEditor g cCoords+ ri = RenderingInput (g ^. landscape . multiWorld) (getEntityIsKnown $ mkEntityKnowledge g)+ terrain = displayTerrainCell worldEditor ri cCoords+ entity = displayEntityCell worldEditor ri cCoords robot = displayRobotCell g cCoords merge = fmap sconcat . NE.nonEmpty . filter (not . (^. invisible))@@ -501,8 +535,8 @@ drawClockDisplay :: Int -> GameState -> Widget n drawClockDisplay lgTPS gs = hBox . intersperse (txt " ") $ catMaybes [clockWidget, pauseWidget] where- clockWidget = maybeDrawTime (gs ^. ticks) (gs ^. paused || lgTPS < 3) gs- pauseWidget = guard (gs ^. paused) $> txt "(PAUSED)"+ clockWidget = maybeDrawTime (gs ^. temporal . ticks) (gs ^. temporal . paused || lgTPS < 3) gs+ pauseWidget = guard (gs ^. temporal . paused) $> txt "(PAUSED)" -- | Check whether the currently focused robot (if any) has a clock -- device equipped.@@ -567,20 +601,13 @@ Nothing -> showFirstCursor s locs Just _ -> Nothing --- | Render the error dialog window with a given error message-renderErrorDialog :: Text -> Widget Name-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)---- | Draw the error dialog window, if it should be displayed right now.+-- | Draw a dialog window, if one should be displayed right now. drawDialog :: AppState -> Widget Name drawDialog s = case s ^. uiState . uiModal of 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)+ Nothing -> emptyWidget -- | Draw one of the various types of modal dialog. drawModal :: AppState -> ModalType -> Widget Name@@ -606,10 +633,41 @@ DescriptionModal e -> descriptionWidget s e QuitModal -> padBottom (Pad 1) $ hCenter $ txt (quitMsg (s ^. uiState . uiMenu)) 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."])+ KeepPlayingModal ->+ padLeftRight 1 $+ displayParagraphs $+ pure+ "Have fun! Hit Ctrl-Q whenever you're ready to proceed to the next challenge or return to the menu." TerrainPaletteModal -> EV.drawTerrainSelector s EntityPaletteModal -> EV.drawEntityPaintSelector s +-- | Render the percentage of ticks that this robot was active.+-- This indicator can take some time to "warm up" and stabilize+-- due to the sliding window.+--+-- == Use of previous tick+-- The 'Swarm.Game.Step.gameTick' function runs all robots, then increments the current tick.+-- So at the time we are rendering a frame, the current tick will always be+-- strictly greater than any ticks stored in the 'WC.WindowedCounter' for any robot;+-- hence 'WC.getOccupancy' will never be @1@ if we use the current tick directly as+-- obtained from the 'ticks' function.+-- So we "rewind" it to the previous tick for the purpose of this display.+renderDutyCycle :: GameState -> Robot -> Widget Name+renderDutyCycle gs robot =+ withAttr dutyCycleAttr . str . flip (showFFloat (Just 1)) "%" $ dutyCyclePercentage+ where+ curTicks = gs ^. temporal . ticks+ window = robot ^. activityCounts . activityWindow++ -- Rewind to previous tick+ latestRobotTick = addTicks (-1) curTicks+ dutyCycleRatio = WC.getOccupancy latestRobotTick window++ dutyCycleAttr = safeIndex dutyCycleRatio meterAttributeNames++ dutyCyclePercentage :: Double+ dutyCyclePercentage = 100 * getValue dutyCycleRatio+ robotsListWidget :: AppState -> Widget Name robotsListWidget s = hCenter table where@@ -621,28 +679,47 @@ . BT.alignRight 4 . BT.table $ map (padLeftRight 1) <$> (headers : robotsTable)- headers =- withAttr robotAttr- <$> [ txt "Name"- , txt "Age"- , txt "Position"- , txt "Inventory"- , txt "Status"- , txt "Log"- ]+ headings =+ [ "Name"+ , "Age"+ , "Pos"+ , "Items"+ , "Status"+ , "Actns"+ , "Cmds"+ , "Cycles"+ , "Activity"+ , "Log"+ ]+ headers = withAttr robotAttr . txt <$> applyWhen cheat ("ID" :) headings robotsTable = mkRobotRow <$> robots mkRobotRow robot =- [ nameWidget- , txt $ from ageStr- , locWidget- , padRight (Pad 1) (txt $ from $ show rInvCount)- , statusWidget- , txt rLog- ]+ applyWhen cheat (idWidget :) cells where- nameWidget = hBox [renderDisplay (robot ^. robotDisplay), higlightSystem . txt $ " " <> robot ^. robotName]- higlightSystem = if robot ^. systemRobot then withAttr highlightAttr else id+ cells =+ [ nameWidget+ , str ageStr+ , locWidget+ , padRight (Pad 1) (str $ show rInvCount)+ , statusWidget+ , str $ show $ robot ^. activityCounts . tangibleCommandCount+ , -- TODO(#1341): May want to expose the details of this histogram in+ -- a per-robot pop-up+ str . show . sum . M.elems $ robot ^. activityCounts . commandsHistogram+ , str $ show $ robot ^. activityCounts . lifetimeStepCount+ , renderDutyCycle (s ^. gameState) robot+ , txt rLog+ ] + idWidget = str $ show $ robot ^. robotID+ nameWidget =+ hBox+ [ renderDisplay (robot ^. robotDisplay)+ , highlightSystem . txt $ " " <> robot ^. robotName+ ]++ highlightSystem = if robot ^. systemRobot then withAttr highlightAttr else id+ ageStr | age < 60 = show age <> "sec" | age < 3600 = show (age `div` 60) <> "min"@@ -750,7 +827,7 @@ availableListWidget gs nl = padTop (Pad 1) $ vBox widgetList where widgetList = case nl of- RecipeList -> mkAvailableList gs availableRecipes renderRecipe+ RecipeList -> mkAvailableList gs (discovery . availableRecipes) renderRecipe MessageList -> messagesWidget gs renderRecipe = padLeftRight 18 . drawRecipe Nothing (fromMaybe E.empty inv) inv = gs ^? to focusedRobot . _Just . robotInventory@@ -776,7 +853,7 @@ , txt wikiCheatSheet ] where- commands = gs ^. availableCommands . notificationsContent+ commands = gs ^. discovery . availableCommands . notificationsContent table = BT.renderTable . BT.surroundingBorder False@@ -798,7 +875,7 @@ map (padTop $ Pad 1) [ txt $ syntax $ constInfo cmd- , padRight (Pad 2) $ txt $ " : " <> prettyText (inferConst cmd)+ , padRight (Pad 2) . withAttr magentaAttr . txt $ " : " <> prettyTextLine (inferConst cmd) , listDevices cmd ] @@ -826,31 +903,35 @@ messagesWidget gs = widgetList where widgetList = focusNewest . map drawLogEntry' $ gs ^. messageNotifications . notificationsContent- focusNewest = if gs ^. paused then id else over _last visible+ focusNewest = if gs ^. temporal . paused then id else over _last visible drawLogEntry' e = withAttr (colorLogs e) $ hBox [ fromMaybe (txt "") $ maybeDrawTime (e ^. leTime) True gs- , padLeft (Pad 2) . txt $ brackets $ e ^. leRobotName+ , padLeft (Pad 2) . txt $ brackets $ e ^. leName , padLeft (Pad 1) . txt2 $ e ^. leText ] txt2 = txtWrapWith indent2 colorLogs :: LogEntry -> AttrName colorLogs e = case e ^. leSource of- Said -> robotColor (e ^. leRobotID)- Logged -> notifAttr- ErrorTrace l -> case l of- Debug -> dimAttr- Warning -> yellowAttr- Error -> redAttr- Critical -> redAttr+ SystemLog -> colorSeverity (e ^. leSeverity)+ RobotLog rls rid _loc -> case rls of+ Said -> robotColor rid+ Logged -> notifAttr+ RobotError -> colorSeverity (e ^. leSeverity) where -- color each robot message with different color of the world- robotColor rid = fgCols !! (rid `mod` fgColLen)- fgCols = map fst worldAttributes- fgColLen = length fgCols+ robotColor = indexWrapNonEmpty worldAttributeNames +colorSeverity :: Severity -> AttrName+colorSeverity = \case+ Info -> infoAttr+ Debug -> dimAttr+ Warning -> yellowAttr+ Error -> redAttr+ Critical -> redAttr+ -- | Draw the F-key modal menu. This is displayed in the top left world corner. drawModalMenu :: AppState -> Widget Name drawModalMenu s = vLimit 1 . hBox $ map (padLeftRight 1 . drawKeyCmd) globalKeyCmds@@ -868,8 +949,8 @@ catMaybes [ Just (NoHighlight, "F1", "Help") , Just (NoHighlight, "F2", "Robots")- , notificationKey availableRecipes "F3" "Recipes"- , notificationKey availableCommands "F4" "Commands"+ , notificationKey (discovery . availableRecipes) "F3" "Recipes"+ , notificationKey (discovery . availableCommands) "F4" "Commands" , notificationKey messageNotifications "F5" "Messages" ] @@ -882,17 +963,18 @@ drawKeyMenu s = vLimit 2 $ hBox- [ vBox- [ mkCmdRow globalKeyCmds- , padLeft (Pad 2) contextCmds- ]+ [ padBottom Max $+ vBox+ [ mkCmdRow globalKeyCmds+ , padLeft (Pad 2) contextCmds+ ] , gameModeWidget ] where mkCmdRow = hBox . map drawPaddedCmd drawPaddedCmd = padLeftRight 1 . drawKeyCmd contextCmds- | ctrlMode == Handling = txt $ fromMaybe "" (s ^? gameState . inputHandler . _Just . _1)+ | ctrlMode == Handling = txt $ fromMaybe "" (s ^? gameState . gameControls . inputHandler . _Just . _1) | otherwise = mkCmdRow focusedPanelCmds focusedPanelCmds = map highlightKeyCmds@@ -901,8 +983,8 @@ . view (uiState . uiFocusRing) $ s - isReplWorking = s ^. gameState . replWorking- isPaused = s ^. gameState . paused+ isReplWorking = s ^. gameState . gameControls . replWorking+ isPaused = s ^. gameState . temporal . paused hasDebug = fromMaybe creative $ s ^? gameState . to focusedRobot . _Just . robotCapabilities . Lens.contains CDebug viewingBase = (s ^. gameState . viewCenterRule) == VCRobot 0 creative = s ^. gameState . creativeMode@@ -912,8 +994,8 @@ inventorySort = s ^. uiState . uiInventorySort inventorySearch = s ^. uiState . uiInventorySearch ctrlMode = s ^. uiState . uiREPL . replControlMode- canScroll = creative || (s ^. gameState . worldScrollable)- handlerInstalled = isJust (s ^. gameState . inputHandler)+ canScroll = creative || (s ^. gameState . landscape . worldScrollable)+ handlerInstalled = isJust (s ^. gameState . gameControls . inputHandler) renderPilotModeSwitch :: ReplControlMode -> T.Text renderPilotModeSwitch = \case@@ -947,9 +1029,7 @@ ] may b = if b then Just else const Nothing - highlightKeyCmds (k, n) = (,k,n) $ case n of- "pop out" | (s ^. uiState . uiMoreInfoBot) || (s ^. uiState . uiMoreInfoTop) -> Alert- _ -> PanelSpecific+ highlightKeyCmds (k, n) = (PanelSpecific, k, n) keyCmdsFor (Just (FocusablePanel WorldEditorPanel)) = [("^s", "save map")]@@ -960,6 +1040,7 @@ ++ [("^c", "cancel") | isReplWorking] ++ [("M-p", renderPilotModeSwitch ctrlMode) | creative] ++ [("M-k", renderHandlerModeSwitch ctrlMode) | handlerInstalled]+ ++ [("PgUp/Dn", "scroll")] keyCmdsFor (Just (FocusablePanel WorldPanel)) = [ ("←↓↑→ / hjkl", "scroll") | canScroll ]@@ -984,7 +1065,7 @@ drawKeyCmd :: (KeyHighlight, Text, Text) -> Widget Name drawKeyCmd (h, key, cmd) = hBox- [ withAttr attr (txt $ T.concat ["[", key, "] "])+ [ withAttr attr (txt $ brackets key) , txt cmd ] where@@ -997,22 +1078,31 @@ -- World panel ------------------------------------------------------------ +worldWidget ::+ (Cosmic W.Coords -> Widget n) ->+ -- | view center+ Cosmic Location ->+ Widget n+worldWidget renderCoord gameViewCenter = Widget Fixed Fixed $+ do+ ctx <- getContext+ let w = ctx ^. availWidthL+ h = ctx ^. availHeightL+ vr = viewingRegion gameViewCenter (fromIntegral w, fromIntegral h)+ ixs = range $ vr ^. planar+ render . vBox . map hBox . chunksOf w . map (renderCoord . Cosmic (vr ^. subworld)) $ ixs+ -- | Draw the current world view.-drawWorld :: UIState -> GameState -> Widget Name-drawWorld ui g =+drawWorldPane :: UIState -> GameState -> Widget Name+drawWorldPane ui g = center . cached WorldCache . reportExtent WorldExtent -- Set the clickable request after the extent to play nice with the cache . clickable (FocusablePanel WorldPanel)- . Widget Fixed Fixed- $ do- ctx <- getContext- let w = ctx ^. availWidthL- h = ctx ^. availHeightL- vr = viewingRegion g (fromIntegral w, fromIntegral h)- ixs = range $ vr ^. planar- render . vBox . map hBox . chunksOf w . map (drawLoc ui g . Cosmic (vr ^. subworld)) $ ixs+ $ worldWidget renderCoord (g ^. viewCenter)+ where+ renderCoord = drawLoc ui g ------------------------------------------------------------ -- Robot inventory panel@@ -1028,17 +1118,17 @@ -- away and a robot that does not exist. | Just r <- s ^. gameState . to focusedRobot , Just (_, lst) <- s ^. uiState . uiInventory =- let Cosmic _subworldName (Location x y) = r ^. robotLocation- drawClickableItem pos selb = clickable (InventoryListItem pos) . drawItem (lst ^. BL.listSelectedL) pos selb+ let drawClickableItem pos selb = clickable (InventoryListItem pos) . drawItem (lst ^. BL.listSelectedL) pos selb+ row =+ [ txt (r ^. robotName)+ , padLeft (Pad 2) . str . renderCoordsString $ r ^. robotLocation+ , padLeft (Pad 2) $ renderDisplay (r ^. robotDisplay)+ ] in padBottom Max $ vBox- [ hCenter $- hBox- [ txt (r ^. robotName)- , padLeft (Pad 2) $ str (printf "(%d, %d)" x y)- , padLeft (Pad 2) $ renderDisplay (r ^. robotDisplay)- ]- , padAll 1 (BL.renderListWithIndex drawClickableItem True lst)+ [ hCenter $ hBox row+ , withLeftPaddedVScrollBars . padLeft (Pad 1) . padTop (Pad 1) $+ BL.renderListWithIndex drawClickableItem True lst ] | otherwise = blank @@ -1089,7 +1179,8 @@ drawInfoPanel s | Just Far <- s ^. gameState . to focusedRange = blank | otherwise =- viewport InfoViewport Vertical+ withVScrollBars OnRight+ . viewport InfoViewport Vertical . padLeftRight 1 $ explainFocusedItem s @@ -1115,6 +1206,7 @@ displayProperties = displayList . mapMaybe showProperty where showProperty Growable = Just "growing"+ showProperty Combustible = Just "combustible" showProperty Infinite = Just "infinite" showProperty Liquid = Just "liquid" showProperty Unwalkable = Just "blocking"@@ -1163,7 +1255,7 @@ -- | Return all recipes that involve a given entity. recipesWith :: AppState -> Entity -> [Recipe Entity] recipesWith s e =- let getRecipes select = recipesFor (s ^. gameState . select) e+ let getRecipes select = recipesFor (s ^. gameState . recipesInfo . select) e in -- The order here is chosen intentionally. See https://github.com/swarm-game/swarm/issues/418. -- -- 1. Recipes where the entity is an input --- these should go@@ -1175,7 +1267,12 @@ -- 3. Recipes where it is an output --- these should go last, -- since if you have it, you probably already figured out how -- to make it.- L.nub $ getRecipes recipesIn ++ getRecipes recipesReq ++ getRecipes recipesOut+ L.nub $+ concat+ [ getRecipes recipesIn+ , getRecipes recipesCat+ , getRecipes recipesOut+ ] -- | Draw an ASCII art representation of a recipe. For now, the -- weight is not shown.@@ -1271,10 +1368,13 @@ where logEntries = s ^. gameState . to focusedRobot . _Just . robotLog - rn = s ^? gameState . to focusedRobot . _Just . robotName+ rid = s ^? gameState . to focusedRobot . _Just . robotID n = Seq.length logEntries - allMe = all ((== rn) . Just . view leRobotName) logEntries+ allMe = all me logEntries+ me le = case le ^. leSource of+ RobotLog _ i _ -> Just i == rid+ _ -> False drawEntry i e = (if i == n - 1 && s ^. uiState . uiScrollToEnd then visible else id) $@@ -1296,10 +1396,18 @@ -- | Draw one log entry with an optional robot name first. drawLogEntry :: Bool -> LogEntry -> Widget a-drawLogEntry addName e = withAttr (colorLogs e) . txtWrapWith indent2 $ if addName then name else t+drawLogEntry addName e =+ withAttr (colorLogs e) . txtWrapWith indent2 $+ if addName then name else t where t = e ^. leText- name = "[" <> view leRobotName e <> "] " <> (if e ^. leSource == Said then "said " <> quote t else t)+ name =+ "["+ <> view leName e+ <> "] "+ <> case e ^. leSource of+ RobotLog Said _ _ -> "said " <> quote t+ _ -> t ------------------------------------------------------------ -- REPL panel@@ -1316,11 +1424,11 @@ | otherwise -> txt $ "[found: \"" <> lastentry <> "\"] " renderREPLPrompt :: FocusRing Name -> REPLState -> Widget Name-renderREPLPrompt focus repl = ps1 <+> replE+renderREPLPrompt focus theRepl = ps1 <+> replE where- prompt = repl ^. replPromptType- replEditor = repl ^. replPromptEditor- color = if repl ^. replValid then id else withAttr redAttr+ prompt = theRepl ^. replPromptType+ replEditor = theRepl ^. replPromptEditor+ color = if theRepl ^. replValid then id else withAttr redAttr ps1 = replPromptAsWidget (T.concat $ getEditContents replEditor) prompt replE = renderEditor@@ -1330,20 +1438,44 @@ -- | Draw the REPL. drawREPL :: AppState -> Widget Name-drawREPL s = vBox $ latestHistory <> [currentPrompt] <> mayDebug+drawREPL s =+ vBox+ [ withLeftPaddedVScrollBars+ . viewport REPLViewport Vertical+ . vBox+ $ [cached REPLHistoryCache (vBox history), currentPrompt]+ , vBox mayDebug+ ] where -- rendered history lines fitting above REPL prompt- latestHistory :: [Widget n]- latestHistory = map fmt (getLatestREPLHistoryItems (replHeight - inputLines - debugLines) (repl ^. replHistory))+ history :: [Widget n]+ history = map fmt . toList . getSessionREPLHistoryItems $ theRepl ^. replHistory currentPrompt :: Widget Name- currentPrompt = case (isActive <$> base, repl ^. replControlMode) of+ currentPrompt = case (isActive <$> base, theRepl ^. replControlMode) of (_, Handling) -> padRight Max $ txt "[key handler running, M-k to toggle]"- (Just False, _) -> renderREPLPrompt (s ^. uiState . uiFocusRing) repl+ (Just False, _) -> renderREPLPrompt (s ^. uiState . uiFocusRing) theRepl _running -> padRight Max $ txt "..."- inputLines = 1- debugLines = 3 * fromEnum (s ^. uiState . uiShowDebug)- repl = s ^. uiState . uiREPL+ theRepl = s ^. uiState . uiREPL base = s ^. gameState . robotMap . at 0 fmt (REPLEntry e) = txt $ "> " <> e fmt (REPLOutput t) = txt t+ fmt (REPLError t) = txtWrapWith indent2 {preserveIndentation = True} t mayDebug = [drawRobotMachine s True | s ^. uiState . uiShowDebug]++------------------------------------------------------------+-- Utility+------------------------------------------------------------++-- See https://github.com/jtdaugherty/brick/discussions/484+withLeftPaddedVScrollBars :: Widget n -> Widget n+withLeftPaddedVScrollBars =+ withVScrollBarRenderer (addLeftSpacing verticalScrollbarRenderer)+ . withVScrollBars OnRight+ where+ addLeftSpacing :: VScrollbarRenderer n -> VScrollbarRenderer n+ addLeftSpacing r =+ r+ { scrollbarWidthAllocation = 2+ , renderVScrollbar = hLimit 1 $ renderVScrollbar r+ , renderVScrollbarTrough = hLimit 1 $ renderVScrollbarTrough r+ }
src/Swarm/TUI/View/Achievement.hs view
@@ -15,9 +15,10 @@ import Swarm.Game.Achievement.Attainment import Swarm.Game.Achievement.Definitions import Swarm.Game.Achievement.Description-import Swarm.TUI.Attr import Swarm.TUI.Model import Swarm.TUI.Model.UI+import Swarm.TUI.View.Attribute.Attr+import Swarm.TUI.View.Util (drawMarkdown) import Text.Wrap padAllEvenly :: Int -> Widget Name -> Widget Name@@ -66,7 +67,7 @@ wasAttained = M.member x attainedMap renderFlavorTextWidget :: FlavorText -> Widget Name- renderFlavorTextWidget (Freeform t) = txtWrap t+ renderFlavorTextWidget (Freeform t) = drawMarkdown t renderFlavorTextWidget (FTQuotation (Quotation author quoteContent)) = vBox [ txtWrap quoteContent@@ -79,10 +80,9 @@ innerContent = vBox [ maybe emptyWidget (padAllEvenly 2 . renderFlavorTextWidget) $ humorousElaboration details- , txtWrap $- if wasAttained || not (isObfuscated details)- then attainmentProcess details- else "???"+ , if wasAttained || not (isObfuscated details)+ then drawMarkdown $ attainmentProcess details+ else txt "???" , case M.lookup x attainedMap of Nothing -> emptyWidget Just attainment ->
+ src/Swarm/TUI/View/Attribute/Attr.hs view
@@ -0,0 +1,229 @@+{-# LANGUAGE OverloadedStrings #-}++-- |+-- SPDX-License-Identifier: BSD-3-Clause+--+-- Rendering attributes (/i.e./ foreground and background colors,+-- styles, /etc./) used by the Swarm TUI.+--+-- We export constants only for those we use in the Haskell code+-- 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 exception.+module Swarm.TUI.View.Attribute.Attr (+ swarmAttrMap,+ worldAttributeNames,+ worldPrefix,+ meterAttributeNames,+ toAttrName,++ -- ** Terrain attributes+ dirtAttr,+ grassAttr,+ stoneAttr,+ waterAttr,+ iceAttr,++ -- ** Common attributes+ entityAttr,+ robotAttr,+ rockAttr,+ plantAttr,++ -- ** Swarm TUI Attributes+ highlightAttr,+ notifAttr,+ infoAttr,+ boldAttr,+ italicAttr,+ dimAttr,+ magentaAttr,+ cyanAttr,+ lightCyanAttr,+ yellowAttr,+ blueAttr,+ greenAttr,+ redAttr,+ defAttr,+ customEditFocusedAttr,+) where++import Brick+import Brick.Forms+import Brick.Widgets.Dialog+import Brick.Widgets.Edit qualified as E+import Brick.Widgets.List hiding (reverse)+import Data.Bifunctor (bimap, first)+import Data.Colour.Palette.BrewerSet+import Data.List.NonEmpty (NonEmpty (..))+import Data.List.NonEmpty qualified as NE+import Data.Maybe (fromMaybe)+import Data.Text (unpack)+import Graphics.Vty qualified as V+import Swarm.Game.Display (Attribute (..))+import Swarm.TUI.View.Attribute.Util++toAttrName :: Attribute -> AttrName+toAttrName = \case+ ARobot -> robotAttr+ AEntity -> entityAttr+ AWorld n -> worldPrefix <> attrName (unpack n)+ ATerrain n -> terrainPrefix <> attrName (unpack n)+ ADefault -> defAttr++-- | A mapping from the defined attribute names to TUI attributes.+swarmAttrMap :: AttrMap+swarmAttrMap =+ attrMap+ V.defAttr+ $ NE.toList activityMeterAttributes+ <> NE.toList (NE.map (first getWorldAttrName) worldAttributes)+ <> [(waterAttr, V.white `on` V.blue)]+ <> terrainAttr+ <> [ -- Robot attribute+ (robotAttr, fg V.white `V.withStyle` V.bold)+ , -- UI rendering attributes+ (highlightAttr, fg V.cyan)+ , (invalidFormInputAttr, fg V.red)+ , (focusedFormInputAttr, V.defAttr)+ , (customEditFocusedAttr, V.black `on` V.yellow)+ , (listSelectedFocusedAttr, bg V.blue)+ , (infoAttr, fg (V.rgbColor @Int 100 100 100))+ , (buttonSelectedAttr, bg V.blue)+ , (notifAttr, fg V.yellow `V.withStyle` V.bold)+ , (dimAttr, V.defAttr `V.withStyle` V.dim)+ , (boldAttr, V.defAttr `V.withStyle` V.bold)+ , (italicAttr, V.defAttr `V.withStyle` V.italic)+ , -- Basic colors+ (redAttr, fg V.red)+ , (greenAttr, fg V.green)+ , (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)+ ]++worldPrefix :: AttrName+worldPrefix = attrName "world"++-- | We introduce this (module-private) newtype+-- so that we can define the 'entity' attribute+-- separate from the list of other 'worldAttributes',+-- while enforcing the convention that both its attribute+-- name and the rest of 'worldAttributes' be consistently+-- prefixed by 'worldPrefix'.+newtype WorldAttr = WorldAttr+ { getWorldAttrName :: AttrName+ }++mkWorldAttr :: String -> WorldAttr+mkWorldAttr = WorldAttr . (worldPrefix <>) . attrName++entity :: (WorldAttr, V.Attr)+entity = (mkWorldAttr "entity", fg V.white)++entityAttr :: AttrName+entityAttr = getWorldAttrName $ fst entity++-- | Colors of entities in the world.+--+-- Also used to color messages, so water is special and excluded.+worldAttributes :: NonEmpty (WorldAttr, V.Attr)+worldAttributes =+ entity+ :| map+ (bimap mkWorldAttr fg)+ [ ("device", V.brightYellow)+ , ("plant", V.green)+ , ("rock", V.rgbColor @Int 80 80 80)+ , ("wood", V.rgbColor @Int 139 69 19)+ , ("flower", V.rgbColor @Int 200 0 200)+ , ("rubber", V.rgbColor @Int 245 224 179)+ , ("copper", V.yellow)+ , ("copper'", V.rgbColor @Int 78 117 102)+ , ("iron", V.rgbColor @Int 97 102 106)+ , ("iron'", V.rgbColor @Int 183 65 14)+ , ("quartz", V.white)+ , ("silver", V.rgbColor @Int 192 192 192)+ , ("gold", V.rgbColor @Int 255 215 0)+ , ("snow", V.white)+ , ("sand", V.rgbColor @Int 194 178 128)+ , ("fire", V.brightRed)+ , ("red", V.red)+ , ("green", V.green)+ , ("blue", V.blue)+ ]++worldAttributeNames :: NonEmpty AttrName+worldAttributeNames = NE.map (getWorldAttrName . fst) worldAttributes++activityMeterPrefix :: AttrName+activityMeterPrefix = attrName "activityMeter"++activityMeterAttributes :: NonEmpty (AttrName, V.Attr)+activityMeterAttributes =+ NE.zip indices $ fromMaybe (pure $ bg V.black) $ NE.nonEmpty brewers+ where+ indices = NE.map ((activityMeterPrefix <>) . attrName . show) $ (0 :: Int) :| [1 ..]+ brewers = map bgWithAutoForeground $ reverse $ brewerSet RdYlGn 7++meterAttributeNames :: NonEmpty AttrName+meterAttributeNames = NE.map fst activityMeterAttributes++terrainPrefix :: AttrName+terrainPrefix = attrName "terrain"++terrainAttr :: [(AttrName, V.Attr)]+terrainAttr =+ [ (dirtAttr, fg (V.rgbColor @Int 165 42 42))+ , (grassAttr, fg (V.rgbColor @Int 0 32 0)) -- dark green+ , (stoneAttr, fg (V.rgbColor @Int 32 32 32))+ , (iceAttr, bg V.white)+ ]++-- | The default robot attribute.+robotAttr :: AttrName+robotAttr = attrName "robot"++dirtAttr, grassAttr, stoneAttr, iceAttr, waterAttr, rockAttr, plantAttr :: AttrName+dirtAttr = terrainPrefix <> attrName "dirt"+grassAttr = terrainPrefix <> attrName "grass"+stoneAttr = terrainPrefix <> attrName "stone"+iceAttr = terrainPrefix <> attrName "ice"+waterAttr = worldPrefix <> attrName "water"+rockAttr = worldPrefix <> attrName "rock"+plantAttr = worldPrefix <> attrName "plant"++-- | Some defined attribute names used in the Swarm TUI.+highlightAttr+ , notifAttr+ , infoAttr+ , boldAttr+ , italicAttr+ , dimAttr+ , defAttr ::+ AttrName+highlightAttr = attrName "highlight"+notifAttr = attrName "notif"+infoAttr = attrName "info"+boldAttr = attrName "bold"+italicAttr = attrName "italics"+dimAttr = attrName "dim"+defAttr = attrName "def"++customEditFocusedAttr :: AttrName+customEditFocusedAttr = attrName "custom" <> E.editFocusedAttr++-- | Some basic colors used in TUI.+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"
+ src/Swarm/TUI/View/Attribute/CustomStyling.hs view
@@ -0,0 +1,37 @@+-- |+-- SPDX-License-Identifier: BSD-3-Clause+module Swarm.TUI.View.Attribute.CustomStyling where++import Brick (AttrName, attrName)+import Data.Colour.SRGB (sRGB24read)+import Data.Set (toList)+import Data.Text qualified as T+import Graphics.Vty.Attributes+import Swarm.Game.Scenario.Style+import Swarm.TUI.View.Attribute.Attr (worldPrefix)+import Swarm.TUI.View.Attribute.Util++toStyle :: StyleFlag -> Style+toStyle = \case+ Standout -> standout+ Italic -> italic+ Strikethrough -> strikethrough+ Underline -> underline+ ReverseVideo -> reverseVideo+ Blink -> blink+ Dim -> dim+ Bold -> bold++hexToAttrColor :: HexColor -> Color+hexToAttrColor (HexColor colorText) =+ kolorToAttrColor c+ where+ c = sRGB24read $ T.unpack colorText++toAttrPair :: CustomAttr -> (AttrName, Attr)+toAttrPair ca =+ (worldPrefix <> attrName (name ca), addStyle $ addFg $ addBg defAttr)+ where+ addFg = maybe id (flip withForeColor . hexToAttrColor) $ fg ca+ addBg = maybe id (flip withBackColor . hexToAttrColor) $ bg ca+ addStyle = maybe id (flip withStyle . sum . map toStyle . toList) $ style ca
+ src/Swarm/TUI/View/Attribute/Util.hs view
@@ -0,0 +1,28 @@+-- |+-- SPDX-License-Identifier: BSD-3-Clause+module Swarm.TUI.View.Attribute.Util where++import Brick.Util (on)+import Data.Colour.CIE (luminance)+import Data.Colour.Palette.BrewerSet (Kolor)+import Data.Colour.SRGB (RGB (..), toSRGB24)+import Graphics.Vty qualified as V+import Graphics.Vty.Attributes++kolorToAttrColor :: Kolor -> Color+kolorToAttrColor c =+ RGBColor r g b+ where+ RGB r g b = toSRGB24 c++-- | Automatically selects black or white for the foreground+-- based on the luminance of the supplied background.+bgWithAutoForeground :: Kolor -> Attr+bgWithAutoForeground c = fgColor `on` kolorToAttrColor c+ where+ fgColor =+ -- "white" is actually gray-ish, so we nudge the threshold+ -- below 0.5.+ if luminance c > 0.4+ then V.black+ else V.white
src/Swarm/TUI/View/CellDisplay.hs view
@@ -15,10 +15,19 @@ import Data.Maybe (maybeToList) import Data.Semigroup (sconcat) import Data.Tagged (unTagged)+import Data.Text (Text) import Data.Word (Word32) import Linear.Affine ((.-.)) import Swarm.Game.CESK (TickNumber (..))-import Swarm.Game.Display+import Swarm.Game.Display (+ Attribute (AEntity),+ Display,+ defaultEntityDisplay,+ displayAttr,+ displayChar,+ displayPriority,+ hidden,+ ) import Swarm.Game.Entity import Swarm.Game.Robot import Swarm.Game.Scenario.Topography.EntityFacade@@ -26,12 +35,12 @@ import Swarm.Game.Terrain import Swarm.Game.Universe import Swarm.Game.World qualified as W-import Swarm.TUI.Attr import Swarm.TUI.Editor.Masking import Swarm.TUI.Editor.Model import Swarm.TUI.Editor.Util qualified as EU import Swarm.TUI.Model.Name import Swarm.TUI.Model.UI+import Swarm.TUI.View.Attribute.Attr import Witch (from) import Witch.Encoding qualified as Encoding @@ -47,16 +56,22 @@ else drawCell where showRobots = ui ^. uiShowRobots- we = ui ^. uiWorldEditor+ we = ui ^. uiWorldEditor . worldOverdraw drawCell = renderDisplay $ displayLoc showRobots we g cCoords +-- | Subset of the game state needed to render the world+data RenderingInput = RenderingInput+ { multiworldInfo :: W.MultiWorld Int Entity+ , isKnownFunc :: EntityPaint -> Bool+ }+ displayTerrainCell ::- WorldEditor Name ->- GameState ->+ WorldOverdraw ->+ RenderingInput -> Cosmic W.Coords -> Display-displayTerrainCell worldEditor g coords =- terrainMap M.! EU.getTerrainAt worldEditor (g ^. multiWorld) coords+displayTerrainCell worldEditor ri coords =+ terrainMap M.! EU.getEditorTerrainAt worldEditor (multiworldInfo ri) coords displayRobotCell :: GameState ->@@ -66,58 +81,82 @@ map (view robotDisplay) $ robotsAtLocation (fmap W.coordsToLoc coords) g -displayEntityCell :: WorldEditor Name -> GameState -> Cosmic W.Coords -> [Display]-displayEntityCell worldEditor g coords =- maybeToList $ displayForEntity <$> maybeEntity- where- (_, maybeEntity) = EU.getContentAt worldEditor (g ^. multiWorld) coords+-- | Extract the relevant subset of information from the 'GameState' to be able+-- to compute whether an entity is "known".+mkEntityKnowledge :: GameState -> EntityKnowledgeDependencies+mkEntityKnowledge gs =+ EntityKnowledgeDependencies+ { isCreativeMode = gs ^. creativeMode+ , globallyKnownEntities = gs ^. discovery . knownEntities+ , theFocusedRobot = focusedRobot gs+ } - displayForEntity :: EntityPaint -> Display- displayForEntity e = (if known e then id else hidden) $ getDisplay e+-- | The subset of information required to compute whether+-- an entity is "known", and therefore should be rendered+-- normally vs as a question mark.+data EntityKnowledgeDependencies = EntityKnowledgeDependencies+ { isCreativeMode :: Bool+ , globallyKnownEntities :: [Text]+ , theFocusedRobot :: Maybe Robot+ } - known (Facade (EntityFacade _ _)) = True- known (Ref e) =- e- `hasProperty` Known- || (e ^. entityName)- `elem` (g ^. knownEntities)- || case hidingMode g of- HideAllEntities -> False- HideNoEntity -> True- HideEntityUnknownTo ro -> ro `robotKnows` e+-- | Determines whether an entity should be rendered+-- normally vs as a question mark.+getEntityIsKnown :: EntityKnowledgeDependencies -> EntityPaint -> Bool+getEntityIsKnown knowledge ep = case ep of+ Facade (EntityFacade _ _) -> True+ Ref e -> or reasonsToShow+ where+ reasonsToShow =+ [ isCreativeMode knowledge+ , e `hasProperty` Known+ , (e ^. entityName) `elem` globallyKnownEntities knowledge+ , showBasedOnRobotKnowledge+ ]+ showBasedOnRobotKnowledge = maybe False (`robotKnows` e) $ theFocusedRobot knowledge -data HideEntity = HideAllEntities | HideNoEntity | HideEntityUnknownTo Robot+displayEntityCell ::+ WorldOverdraw ->+ RenderingInput ->+ Cosmic W.Coords ->+ [Display]+displayEntityCell worldEditor ri coords =+ maybeToList $ displayForEntity <$> maybeEntity+ where+ (_, maybeEntity) = EU.getEditorContentAt worldEditor (multiworldInfo ri) coords -hidingMode :: GameState -> HideEntity-hidingMode g- | g ^. creativeMode = HideNoEntity- | otherwise = maybe HideAllEntities HideEntityUnknownTo $ focusedRobot g+ displayForEntity :: EntityPaint -> Display+ displayForEntity e = (if isKnownFunc ri e then id else hidden) $ getDisplay e -- | Get the 'Display' for a specific location, by combining the -- 'Display's for the terrain, entity, and robots at the location, and -- taking into account "static" based on the distance to the robot -- being @view@ed.-displayLoc :: Bool -> WorldEditor Name -> GameState -> Cosmic W.Coords -> Display+displayLoc :: Bool -> WorldOverdraw -> GameState -> Cosmic W.Coords -> Display displayLoc showRobots we g cCoords@(Cosmic _ coords) = staticDisplay g coords- <> displayLocRaw showRobots we g cCoords+ <> displayLocRaw we ri robots cCoords+ where+ ri = RenderingInput (g ^. landscape . multiWorld) (getEntityIsKnown $ mkEntityKnowledge g)+ robots =+ if showRobots+ then displayRobotCell g cCoords+ else [] -- | Get the 'Display' for a specific location, by combining the -- 'Display's for the terrain, entity, and robots at the location. displayLocRaw ::- Bool ->- WorldEditor Name ->- GameState ->+ WorldOverdraw ->+ RenderingInput ->+ -- | Robot displays+ [Display] -> Cosmic W.Coords -> Display-displayLocRaw showRobots worldEditor g coords = sconcat $ terrain NE.:| entity <> robots+displayLocRaw worldEditor ri robotDisplays coords =+ sconcat $ terrain NE.:| entity <> robotDisplays where- terrain = displayTerrainCell worldEditor g coords- entity = displayEntityCell worldEditor g coords- robots =- if showRobots- then displayRobotCell g coords- else []+ terrain = displayTerrainCell worldEditor ri coords+ entity = displayEntityCell worldEditor ri coords -- | Random "static" based on the distance to the robot being -- @view@ed.@@ -172,7 +211,7 @@ murmur3 1 . unTagged . from @String @(Encoding.UTF_8 ByteString) . show $ -- include the current tick count / 16 in the hash, so the pattern of static -- changes once every 16 ticks- (offset, getTickNumber (g ^. ticks) `div` 16)+ (offset, getTickNumber (g ^. temporal . ticks) `div` 16) -- Hashed probability, i.e. convert the hash into a floating-point number between 0 and 1 hp :: Double
− src/Swarm/TUI/View/CustomStyling.hs
@@ -1,38 +0,0 @@--- |--- SPDX-License-Identifier: BSD-3-Clause-module Swarm.TUI.View.CustomStyling where--import Brick (AttrName, attrName)-import Data.Colour.SRGB (Colour, RGB (..), sRGB24read, toSRGB24)-import Data.Set (toList)-import Data.Text qualified as T-import Graphics.Vty.Attributes-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) =- RGBColor r g b- where- RGB r g b = toSRGB24 c- c :: Colour Double- c = sRGB24read $ T.unpack colorText--toAttrPair :: CustomAttr -> (AttrName, Attr)-toAttrPair ca =- (worldPrefix <> attrName (name ca), addStyle $ addFg $ addBg defAttr)- 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/Objective.hs view
@@ -18,9 +18,9 @@ import Data.Vector qualified as V import Swarm.Game.Scenario.Objective import Swarm.Language.Text.Markdown qualified as Markdown-import Swarm.TUI.Attr import Swarm.TUI.Model.Goal import Swarm.TUI.Model.Name+import Swarm.TUI.View.Attribute.Attr import Swarm.TUI.View.Util makeListWidget :: GoalTracking -> BL.List Name GoalEntry@@ -34,12 +34,16 @@ renderGoalsDisplay gd = if hasMultiple then- hBox- [ leftSide- , hLimitPercent 70 $ padLeft (Pad 2) goalElaboration+ vBox+ [ hBox+ [ leftSide+ , hLimitPercent 70 $ padLeft (Pad 2) goalElaboration+ ]+ , footer ] else goalElaboration where+ footer = hCenter $ withAttr italicAttr $ txt "NOTE: [Tab] toggles focus between panes" hasMultiple = hasMultipleGoals $ gd ^. goalsContent lw = _listWidget gd fr = _focus gd
src/Swarm/TUI/View/Util.hs view
@@ -25,9 +25,9 @@ import Swarm.Language.Syntax (Syntax) import Swarm.Language.Text.Markdown qualified as Markdown import Swarm.Language.Types (Polytype)-import Swarm.TUI.Attr import Swarm.TUI.Model import Swarm.TUI.Model.UI+import Swarm.TUI.View.Attribute.Attr import Swarm.TUI.View.CellDisplay import Swarm.Util (listEnums) import Witch (from, into)
src/Swarm/Util.hs view
@@ -15,6 +15,8 @@ maximum0, cycleEnum, listEnums,+ listEnumsNonempty,+ showEnum, indexWrapNonEmpty, uniq, binTuples,@@ -24,6 +26,8 @@ allEqual, surfaceEmpty, applyWhen,+ hoistMaybe,+ unsnocNE, -- * Directory utilities readFileMay,@@ -78,6 +82,7 @@ import Control.Effect.State (State, modify, state) import Control.Lens (ASetter', Lens', LensLike, LensLike', Over, lens, (<&>), (<>~)) import Control.Monad (filterM, guard, unless)+import Control.Monad.Trans.Maybe (MaybeT (..)) import Data.Bifunctor (Bifunctor (bimap), first) import Data.Char (isAlphaNum, toLower) import Data.Either.Validation@@ -142,8 +147,28 @@ listEnums :: (Enum e, Bounded e) => [e] listEnums = [minBound .. maxBound] --- | Guaranteed to yield an element of the list-indexWrapNonEmpty :: Integral b => NonEmpty a -> b -> a+-- | Members of the Bounded class are guaranteed to+-- have at least one element.+listEnumsNonempty :: (Enum e, Bounded e) => NonEmpty e+listEnumsNonempty = NE.fromList listEnums++-- | We know by the syntax rules of Haskell that constructor+-- names must consist of one or more symbols!+showEnum :: (Show e, Enum e) => e -> NonEmpty Char+showEnum = NE.fromList . show++-- | Guaranteed to yield an element of the list.+--+-- This is true even if the supplied @index@ is negative,+-- since 'mod' always satisfies @0 <= a `mod` b < b@+-- when @b@ is positive+-- (see <comment https://github.com/swarm-game/swarm/pull/1181#discussion_r1151177735>).+indexWrapNonEmpty ::+ Integral b =>+ NonEmpty a ->+ -- | index+ b ->+ a indexWrapNonEmpty list idx = NE.toList list !! fromIntegral wrappedIdx where@@ -200,11 +225,37 @@ surfaceEmpty :: Alternative f => (a -> Bool) -> a -> f a surfaceEmpty isEmpty t = t <$ guard (not (isEmpty t)) +------------------------------------------------------------+-- Forward-compatibility functions+ -- Note, once we upgrade to an LTS version that includes -- base-compat-0.13, we should switch to using 'applyWhen' from there. applyWhen :: Bool -> (a -> a) -> a -> a applyWhen True f x = f x applyWhen False _ x = x++-- | Convert a 'Maybe' computation to 'MaybeT'.+--+-- TODO (#1151): Use implementation from "transformers" package v0.6.0.0+hoistMaybe :: (Applicative m) => Maybe b -> MaybeT m b+hoistMaybe = MaybeT . pure++-- | Like 'unsnoc', but for 'NonEmpty' so without the 'Maybe'+--+-- Taken from Cabal-syntax Distribution.Utils.Generic.+--+-- Example:+-- >>> import Data.List.NonEmpty (NonEmpty ((:|)))+-- >>> unsnocNE (1 :| [2, 3])+-- ([1,2],3)+--+-- >>> unsnocNE (1 :| [])+-- ([],1)+unsnocNE :: NonEmpty a -> ([a], a)+unsnocNE (x :| xs) = go x xs+ where+ go y [] = ([], y)+ go y (z : zs) = let ~(ws, w) = go z zs in (y : ws, w) ------------------------------------------------------------ -- Directory stuff
+ src/Swarm/Util/UnitInterval.hs view
@@ -0,0 +1,38 @@+-- |+-- SPDX-License-Identifier: BSD-3-Clause+--+-- Creation and utilities for the unit interval+module Swarm.Util.UnitInterval (+ UnitInterval,+ getValue,+ mkInterval,+ safeIndex,+) where++import Data.List.NonEmpty (NonEmpty, (!!))+import Prelude hiding ((!!))++newtype UnitInterval a = UnitInterval+ { getValue :: a+ }++-- | Guarantees that the stored value falls within the closed interval+-- @[0, 1]@. It is up to clients to ensure that the promotion+-- to this type is lossless.+mkInterval :: (Ord a, Num a) => a -> UnitInterval a+mkInterval = UnitInterval . max 0 . min 1++-- | Since '(!!)' is partial, here is "proof" that it is safe:+-- If "alpha" is its maximum value of @1@, then the maximum value+-- of the computed index shall be one less than the length of the+-- list (i.e., a valid index).+--+-- See also: 'Swarm.Util.indexWrapNonEmpty'.+safeIndex ::+ RealFrac a =>+ -- | alpha+ UnitInterval a ->+ NonEmpty b ->+ b+safeIndex (UnitInterval alpha) xs =+ xs !! floor (alpha * fromIntegral (length xs - 1))
+ src/Swarm/Util/WindowedCounter.hs view
@@ -0,0 +1,189 @@+{-# LANGUAGE OverloadedStrings #-}++-- |+-- SPDX-License-Identifier: BSD-3-Clause+--+-- Sliding window for activity monitoring.+module Swarm.Util.WindowedCounter (+ WindowedCounter,+ Offsettable (..),++ -- * Construction+ mkWindow,++ -- * Querying+ getOccupancy,++ -- * Maintenance+ insert,+ discardGarbage,+) where++import Data.Aeson+import Data.Set (Set)+import Data.Set qualified as Set+import Swarm.Util.UnitInterval+import Prelude hiding (length)++-- | Values that can be offset by an integral amount+class Offsettable a where+ offsetBy :: Int -> a -> a++-- | A "sliding window" of a designated span that supports insertion+-- of tick "timestamps" that represent some state of interest during that tick.+-- This data structure supports efficient querying of the ratio of+-- {ticks for which that state existed}+-- to the+-- {total number of ticks spanned by the window}.+--+-- The primary use case is in displaying the "activity level" of a robot.+--+-- == Efficiency considerations+--+-- The data retention of the window shall be maintained externally by+-- invoking the 'discardGarbage' function. However, we should not+-- unconditionally invoke this function upon each game tick.+--+-- For efficiency, we do not want to iterate over every robot+-- upon every tick; we only want to "visit" a robot if it is actually+-- doing work that tick.+-- Because of this, there may be some ticks in which the oldest element+-- that is still stored falls outside of the nominal retention window+-- while a robot is inactive.+--+-- One might think we could perform garbage collection whenever we execute queries.+-- However, in the context in which the view powered by the query is generated, we+-- are not permitted to mutate the "state" of the game+-- (the type signature of the rendering function is @AppState -> Widget Name@).+--+-- Therefore, when we perform "queries" on the window, we must apply some+-- filtering to exclude the "stragglers"; data members that have already fallen outside+-- the window but have not yet been "garbage collected".+-- We use a 'Set' to allow this filtering to be performed in @O(log n)@ time.+--+-- In the worst case, the entire dataset may "age out" without being garbage collected,+-- so that an @O(log n)@ filtering operation might be performed upon every "frame refresh"+-- of the UI view.+-- However, we also store the largest element of the window separately from the 'Set' so that+-- we can compare against it for a @O(1)@ short-circuited path once every member ages out.+--+-- The maximum number of elements ever stored in the 'Set' will be the width of the nominal+-- span, even after some protracted failure to "garbage collect".+data WindowedCounter a = WindowedCounter+ { _members :: Set a+ , _lastLargest :: Maybe a+ -- ^ NOTE: It is possible that '_lastLargest' may not exist in the 'Set'.+ , _nominalSpan :: Int+ -- ^ Data retention window. This value is guaranteed positive by the smart constructor.+ }+ deriving (Eq, Show)++-- | Automatically deriving 'FromJSON' circumvents the protection offered by "smart constructors",+-- and the 'ToJSON' instance may expose internal details.+-- Therefore, we write our own custom implementations.+--+-- This 'ToJSON' instance is strictly for diagnostic purposes, and we can reveal+-- a bit more information than is used for parsing.+instance (ToJSON a) => ToJSON (WindowedCounter a) where+ toJSON (WindowedCounter membersSet _lastLargest nominalSpan) =+ object+ [ "members" .= toJSON membersSet+ , "span" .= nominalSpan+ ]++-- | We discard any "internal state" revealed by the 'ToJSON' instance and+-- just use the "span" so that we can rely on any guarantees offered by the+-- smart constructor, no matter the origin of the JSON.+--+-- Discarding the internal state is OK, because it is not integral to gameplay;+-- it is merely informational as a live indicator in the UI.+instance FromJSON (WindowedCounter a) where+ parseJSON = withObject "WindowedCounter" $ \v -> do+ s <- v .: "span"+ return $ mkWindow s++-- | NOTE: We take the absolute value of the "window span" argument+-- so that we can make guarantees about the output of 'getOccupancy'.+mkWindow ::+ -- | window span+ Int ->+ WindowedCounter a+mkWindow = WindowedCounter Set.empty Nothing . abs++-- | Return the ratio of {members in the window} to the {integral span+-- represented by the window}.+--+-- The "current time" should be at least as large as the largest+-- element of the window.+--+-- A fully-contiguous collection of ticks would have an occupancy ratio of @1@.+--+-- == Unit interval guarantee+-- The returned ratio is /guaranteed/ to lie on the unit interval, because:+--+-- * Both the numerator and denominator of the ratio are guaranteed positive, and+-- * 'discardGarbage' guarantees that the set size is less than or equal to+-- the nominal span.+getOccupancy ::+ (Ord a, Offsettable a) =>+ -- | current time+ a ->+ WindowedCounter a ->+ UnitInterval Double+getOccupancy currentTime wc@(WindowedCounter s lastLargest nominalSpan) =+ mkInterval $+ if Set.null s || maybe False (< referenceTick) lastLargest+ then 0+ else fromIntegral (Set.size culledSet) / fromIntegral nominalSpan+ where+ referenceTick = offsetBy (negate nominalSpan) currentTime+ -- Cull the window according to the current time+ WindowedCounter culledSet _ _ = discardGarbage currentTime wc++-- | Invocations of this function shall be guarded externally+-- by the conditions meant to be tracked in the window.+--+-- Proper usage dictates that the value inserted should always+-- be at least as large as the current largest element of the set.+--+-- The 'discardGarbage' function is called from inside this function+-- so that maintenance of the data structure is simplified.+insert ::+ (Ord a, Offsettable a) =>+ -- | current time+ a ->+ WindowedCounter a ->+ WindowedCounter a+insert x (WindowedCounter s lastLargest nominalSpan) =+ discardGarbage x $ WindowedCounter (Set.insert x s) newLargest nominalSpan+ where+ newLargest = Just $ maybe x (max x) lastLargest++-- | Drop the leading elements that are not larger than the cutoff.+--+-- This function is already called by the 'insert' function, so clients+-- do not necessarily ever have to call this directly.+-- However, there may+-- be opportunity to call this even more often, i.e. in code paths where the+-- robot is visited but the condition for insertion is not met.+--+-- == Invariant+-- If the largest member of the set is the current time,+-- then after calling this function, the difference between smallest and largest+-- value in the set is strictly less than the "nominal span", and the size of the+-- set is less than or equal to the nominal span.+--+-- For example, if the nominal span is @3@, the current time is @7@, and the+-- set entails a contiguous sequence @{2, 3, 4, 5, 6, 7}@, then the pivot for 'Set.split' will be+-- @7 - 3 = 4@. The set becomes @{5, 6, 7}@, with cardinality equal to the nominal span.+discardGarbage ::+ (Ord a, Offsettable a) =>+ -- | current time+ a ->+ WindowedCounter a ->+ WindowedCounter a+discardGarbage currentTime (WindowedCounter s lastLargest nominalSpan) =+ WindowedCounter larger lastLargest nominalSpan+ where+ -- NOTE: Neither output set of 'split' includes the "pivot" value.+ (_smaller, larger) = Set.split (offsetBy (negate nominalSpan) currentTime) s
src/Swarm/Version.hs view
@@ -142,6 +142,12 @@ tagToVersion :: String -> Version tagToVersion = fst . last . readP_to_S parseVersion +-- | Drop trailing zeros from versions so that we can compare them.+normalize :: Version -> Version+normalize (Version ns tags) = Version (dropTrailing0 ns) tags+ where+ dropTrailing0 = reverse . dropWhile (== 0) . reverse+ -- | Get a newer upstream release version. -- -- This function can fail if the current branch is not main,@@ -161,6 +167,6 @@ getUpVer :: String -> Either NewReleaseFailure String getUpVer upTag = let upVer = tagToVersion upTag- in if myVer >= upVer+ in if normalize myVer >= normalize upVer then Left $ OldUpstreamRelease upVer myVer else Right upTag
src/Swarm/Web.hs view
@@ -11,17 +11,29 @@ -- The service can be started using the `--port 5357` command line argument, -- or through the REPL by calling `Swarm.App.demoWeb`. ----- Once running, here are the available endpoints:------ * /robots : return the list of robots--- * /robot/ID : return a single robot identified by its id+-- See 'SwarmAPI' for the available endpoints. You can also see them in your+-- browser on the top level endpoint:+-- @lynx localhost:5357 -dump@+-- or you can output the markdown documentation to your terminal:+-- @cabal run swarm -O0 -- generate endpoints@ -- -- Missing endpoints: -- -- * TODO: #625 run endpoint to load definitions -- * TODO: #493 export the whole game state-module Swarm.Web where+module Swarm.Web (+ startWebThread,+ defaultPort, + -- ** Docs+ SwarmAPI,+ swarmApiHtml,+ swarmApiMarkdown,++ -- ** Development+ webMain,+) where+ import Brick.BChan import Commonmark qualified as Mark (commonmark, renderHtml) import Control.Arrow (left)@@ -46,6 +58,7 @@ import Servant import Servant.Docs (ToCapture) import Servant.Docs qualified as SD+import Servant.Docs.Internal qualified as SD (renderCurlBasePath) import Swarm.Game.Robot import Swarm.Game.Scenario.Objective import Swarm.Game.Scenario.Objective.Graph@@ -53,7 +66,7 @@ import Swarm.Game.State import Swarm.Language.Module import Swarm.Language.Pipeline-import Swarm.Language.Pretty (prettyString)+import Swarm.Language.Pretty (prettyTextLine) import Swarm.Language.Syntax import Swarm.ReadableIORef import Swarm.TUI.Model@@ -63,13 +76,11 @@ import Text.Read (readEither) import Witch (into) -newtype RobotID = RobotID Int--instance FromHttpApiData RobotID where- parseUrlPiece = fmap RobotID . left T.pack . readEither . T.unpack+-- ------------------------------------------------------------------+-- Docs+-- ------------------------------------------------------------------ -instance SD.ToSample T.Text where- toSamples _ = SD.noSamples+newtype RobotID = RobotID Int type SwarmAPI = "robots" :> Get '[JSON] [Robot]@@ -83,12 +94,6 @@ :<|> "code" :> "run" :> ReqBody '[PlainText] T.Text :> Post '[PlainText] T.Text :<|> "repl" :> "history" :> "full" :> Get '[JSON] [REPLHistItem] -instance ToCapture (Capture "id" RobotID) where- toCapture _ =- SD.DocCapture- "id" -- name- "(integer) robot ID" -- description- swarmApi :: Proxy SwarmAPI swarmApi = Proxy @@ -97,87 +102,119 @@ api :: Proxy ToplevelAPI api = Proxy -docsBS :: ByteString-docsBS =+swarmApiHtml :: ByteString+swarmApiHtml = encodeUtf8 . either (error . show) (Mark.renderHtml @()) . Mark.commonmark ""- . T.pack- . SD.markdownWith- ( SD.defRenderingOptions- & SD.requestExamples .~ SD.FirstContentType- & SD.responseExamples .~ SD.FirstContentType- )+ $ T.pack swarmApiMarkdown++swarmApiMarkdown :: String+swarmApiMarkdown =+ SD.markdownWith+ ( SD.defRenderingOptions+ & SD.requestExamples .~ SD.FirstContentType+ & SD.responseExamples .~ SD.FirstContentType+ & SD.renderCurlBasePath ?~ "http://localhost:" <> show defaultPort+ ) $ SD.docsWithIntros [intro] swarmApi where intro = SD.DocIntro "Swarm Web API" ["All of the valid endpoints are documented below."] +-- ------------------------------------------------------------------+-- Handlers+-- ------------------------------------------------------------------+ mkApp :: ReadableIORef AppState ->- -- | Writable+ -- | Writable channel to send events to the game BChan AppEvent -> Servant.Server SwarmAPI-mkApp appStateRef chan =- robotsHandler- :<|> robotHandler- :<|> prereqsHandler- :<|> activeGoalsHandler- :<|> goalsGraphHandler- :<|> uiGoalHandler- :<|> goalsHandler+mkApp state events =+ robotsHandler state+ :<|> robotHandler state+ :<|> prereqsHandler state+ :<|> activeGoalsHandler state+ :<|> goalsGraphHandler state+ :<|> uiGoalHandler state+ :<|> goalsHandler state :<|> codeRenderHandler- :<|> codeRunHandler- :<|> replHandler- where- robotsHandler = do- appState <- liftIO (readIORef appStateRef)- pure $ IM.elems $ appState ^. gameState . robotMap- robotHandler (RobotID rid) = do- 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- codeRenderHandler contents = do- return $ case processTermEither contents of- Right (ProcessedTerm (Module stx@(Syntax' _srcLoc _term _) _) _ _) ->- into @Text . drawTree . fmap prettyString . para Node $ stx- Left x -> x- codeRunHandler contents = do- liftIO . writeBChan chan . Web $ RunWebCode contents- return $ T.pack "Sent\n"- replHandler = do- appState <- liftIO (readIORef appStateRef)- let replHistorySeq = appState ^. uiState . uiREPL . replHistory . replSeq- items = toList replHistorySeq- pure items+ :<|> codeRunHandler events+ :<|> replHandler state +robotsHandler :: ReadableIORef AppState -> Handler [Robot]+robotsHandler appStateRef = do+ appState <- liftIO (readIORef appStateRef)+ pure $ IM.elems $ appState ^. gameState . robotMap++robotHandler :: ReadableIORef AppState -> RobotID -> Handler (Maybe Robot)+robotHandler appStateRef (RobotID rid) = do+ appState <- liftIO (readIORef appStateRef)+ pure $ IM.lookup rid (appState ^. gameState . robotMap)++prereqsHandler :: ReadableIORef AppState -> Handler [PrereqSatisfaction]+prereqsHandler appStateRef = do+ appState <- liftIO (readIORef appStateRef)+ case appState ^. gameState . winCondition of+ WinConditions _winState oc -> return $ getSatisfaction oc+ _ -> return []++activeGoalsHandler :: ReadableIORef AppState -> Handler [Objective]+activeGoalsHandler appStateRef = do+ appState <- liftIO (readIORef appStateRef)+ case appState ^. gameState . winCondition of+ WinConditions _winState oc -> return $ getActiveObjectives oc+ _ -> return []++goalsGraphHandler :: ReadableIORef AppState -> Handler (Maybe GraphInfo)+goalsGraphHandler appStateRef = do+ appState <- liftIO (readIORef appStateRef)+ return $ case appState ^. gameState . winCondition of+ WinConditions _winState oc -> Just $ makeGraphInfo oc+ _ -> Nothing++uiGoalHandler :: ReadableIORef AppState -> Handler GoalTracking+uiGoalHandler appStateRef = do+ appState <- liftIO (readIORef appStateRef)+ return $ appState ^. uiState . uiGoal . goalsContent++goalsHandler :: ReadableIORef AppState -> Handler WinCondition+goalsHandler appStateRef = do+ appState <- liftIO (readIORef appStateRef)+ return $ appState ^. gameState . winCondition++codeRenderHandler :: Text -> Handler Text+codeRenderHandler contents = do+ return $ case processTermEither contents of+ Right (ProcessedTerm (Module stx@(Syntax' _srcLoc _term _) _) _ _) ->+ into @Text . drawTree . fmap (T.unpack . prettyTextLine) . para Node $ stx+ Left x -> x++codeRunHandler :: BChan AppEvent -> Text -> Handler Text+codeRunHandler chan contents = do+ liftIO . writeBChan chan . Web $ RunWebCode contents+ return $ T.pack "Sent\n"++replHandler :: ReadableIORef AppState -> Handler [REPLHistItem]+replHandler appStateRef = do+ appState <- liftIO (readIORef appStateRef)+ let replHistorySeq = appState ^. uiState . uiREPL . replHistory . replSeq+ items = toList replHistorySeq+ pure items++-- ------------------------------------------------------------------+-- Main app (used by service and for development)+-- ------------------------------------------------------------------+ -- | Simple result type to report errors from forked startup thread. data WebStartResult = WebStarted | WebStartError String webMain :: Maybe (MVar WebStartResult) -> Warp.Port ->+ -- | Read-only reference to the application state. ReadableIORef AppState ->- -- | Writable+ -- | Writable channel to send events to the game BChan AppEvent -> IO () webMain baton port appStateRef chan = catch (Warp.runSettings settings app) handleErr@@ -191,7 +228,7 @@ server = mkApp appStateRef chan :<|> Tagged serveDocs where serveDocs _ resp =- resp $ responseLBS ok200 [plain] docsBS+ resp $ responseLBS ok200 [plain] swarmApiHtml plain = ("Content-Type", "text/html") app :: Network.Wai.Application@@ -202,6 +239,10 @@ Just mv -> putMVar mv (WebStartError $ displayException e) Nothing -> throwIO e +-- ------------------------------------------------------------------+-- Web service+-- ------------------------------------------------------------------+ defaultPort :: Warp.Port defaultPort = 5357 @@ -233,3 +274,22 @@ Nothing -> case userPort of Just _p -> fail failMsg Nothing -> return . Left $ failMsg <> " (timeout)"++-- ------------------------------------------------------------------+-- Necessary instances+-- ------------------------------------------------------------------++instance SD.ToSample T.Text where+ toSamples _ = SD.noSamples++instance FromHttpApiData RobotID where+ parseUrlPiece = fmap RobotID . left T.pack . readEither . T.unpack++instance SD.ToSample RobotID where+ toSamples _ = SD.samples [RobotID 0, RobotID 1]++instance ToCapture (Capture "id" RobotID) where+ toCapture _ =+ SD.DocCapture+ "id" -- name+ "(integer) robot ID" -- description
swarm.cabal view
@@ -1,15 +1,29 @@ cabal-version: 2.4 name: swarm-version: 0.4+version: 0.5.0.0 synopsis: 2D resource gathering game with programmable robots description: Swarm is a 2D programming and resource gathering game. Program your robots to explore the world and collect resources, which in turn allows you to build upgraded robots that can run more- interesting and complex programs. See the README+ interesting and complex programs. See the+ <https://github.com/swarm-game/swarm/blob/main/README.md README> for more information and instructions on how to play or contribute!+ .+ == Module organization+ For developers getting oriented, Swarm's modules are organized into+ roughly the following layers, from inner to outer:+ .+ * utilities+ * swarm language+ * swarm game+ * swarm TUI+ * swarm app+ .+ To maintain this separation, inner layers should avoid introducing+ dependencies on layers above them. license: BSD-3-Clause license-file: LICENSE@@ -23,6 +37,8 @@ example/*.sw editors/emacs/*.el editors/vscode/syntaxes/*.json+ editors/vim/*.vim+ editors/vim/*.lua data-dir: data/ data-files: *.yaml, worlds/*.world, scenarios/**/*.yaml, scenarios/**/*.txt, scenarios/**/*.sw, *.txt, test/language-snippets/**/*.sw @@ -87,6 +103,12 @@ Swarm.Constant Swarm.Doc.Gen Swarm.Doc.Pedagogy+ Swarm.Doc.Schema.Arrangement+ Swarm.Doc.Schema.Parse+ Swarm.Doc.Schema.Refined+ Swarm.Doc.Schema.Render+ Swarm.Doc.Schema.SchemaType+ Swarm.Doc.Util Swarm.Game.Failure Swarm.Game.Achievement.Attainment Swarm.Game.Achievement.Definitions@@ -97,13 +119,13 @@ Swarm.Game.Entity Swarm.Game.Exception Swarm.Game.Location- Swarm.Game.Log Swarm.Game.Recipe Swarm.Game.ResourceLoading Swarm.Game.Robot Swarm.Game.Scenario Swarm.Game.Scenario.Topography.Cell Swarm.Game.Universe+ Swarm.Log Swarm.TUI.Launch.Controller Swarm.TUI.Launch.Model Swarm.TUI.Launch.Prep@@ -122,6 +144,7 @@ Swarm.Game.Scenario.Style Swarm.Game.Scenario.Topography.EntityFacade Swarm.Game.Scenario.Topography.Navigation.Portal+ Swarm.Game.Scenario.Topography.Navigation.Util Swarm.Game.Scenario.Topography.Navigation.Waypoint Swarm.Game.Scenario.Topography.Placement Swarm.Game.Scenario.Topography.Structure@@ -130,6 +153,9 @@ Swarm.Game.ScenarioInfo Swarm.Game.State Swarm.Game.Step+ Swarm.Game.Step.Combustion+ Swarm.Game.Step.Pathfinding+ Swarm.Game.Step.Util Swarm.Game.Terrain Swarm.Game.Value Swarm.Game.World@@ -140,7 +166,9 @@ Swarm.Game.World.Gen Swarm.Game.World.Interpret Swarm.Game.World.Load+ Swarm.Game.World.Modify Swarm.Game.World.Parse+ Swarm.Game.World.Render Swarm.Game.World.Syntax Swarm.Game.World.Typecheck Swarm.Language.Capability@@ -166,7 +194,9 @@ Swarm.Language.Types Swarm.Language.Value Swarm.ReadableIORef- Swarm.TUI.Attr+ Swarm.TUI.View.Attribute.CustomStyling+ Swarm.TUI.View.Attribute.Attr+ Swarm.TUI.View.Attribute.Util Swarm.TUI.Border Swarm.Game.Scenario.Topography.Area Swarm.TUI.Editor.Controller@@ -191,7 +221,6 @@ Swarm.TUI.View Swarm.TUI.View.Achievement Swarm.TUI.View.CellDisplay- Swarm.TUI.View.CustomStyling Swarm.TUI.View.Objective Swarm.TUI.View.Util Swarm.Util@@ -199,6 +228,8 @@ Swarm.Util.Erasable Swarm.Util.Lens Swarm.Util.Parse+ Swarm.Util.UnitInterval+ Swarm.Util.WindowedCounter Swarm.Util.Yaml Swarm.Version Swarm.Web@@ -209,9 +240,10 @@ brick-list-skip >= 0.1.1.2 && < 0.2, aeson >= 2 && < 2.2, array >= 0.5.4 && < 0.6,+ astar >= 0.3 && < 0.3.1, blaze-html >= 0.9.1 && < 0.9.2, boolexpr >= 0.2 && < 0.3,- brick >= 1.5 && < 1.10,+ brick >= 1.10 && < 1.11, bytestring >= 0.10 && < 0.12, clock >= 0.8.2 && < 0.9, colour >= 2.3.6 && < 2.4,@@ -235,15 +267,19 @@ lens >= 4.19 && < 5.3, linear >= 1.21.6 && < 1.23, lsp >= 1.6 && < 1.7,- megaparsec >= 9.0 && < 9.5,+ megaparsec >= 9.0 && < 9.6, minimorph >= 0.3 && < 0.4, transformers >= 0.5 && < 0.7, mtl >= 2.2.2 && < 2.4,+ pandoc >= 3.0 && < 3.2,+ pandoc-types >= 1.23 && < 1.24, murmur3 >= 1.0.4 && < 1.1, natural-sort >= 0.1.2 && < 0.2,+ palette >= 0.3 && < 0.4, parser-combinators >= 1.2 && < 1.4, prettyprinter >= 1.7.0 && < 1.8, random >= 1.2.0 && < 1.3,+ scientific >= 0.3.6 && < 0.3.8, servant >= 0.19 && < 0.21, servant-docs >= 0.12 && < 0.14, servant-server >= 0.19 && < 0.21,@@ -281,9 +317,11 @@ main-is: Main.hs build-depends: optparse-applicative >= 0.16 && < 0.19, githash >= 0.1.6 && < 0.2,+ terminal-size >= 0.3 && < 1.0, -- Imports shared with the library don't need bounds base, text,+ prettyprinter, swarm hs-source-dirs: app default-language: Haskell2010@@ -307,7 +345,7 @@ TestScoring TestUtil - build-depends: tasty >= 0.10 && < 1.5,+ build-depends: tasty >= 0.10 && < 1.6, tasty-hunit >= 0.10 && < 0.11, tasty-quickcheck >= 0.10 && < 0.11, QuickCheck >= 2.14 && < 2.15,@@ -335,7 +373,7 @@ main-is: Main.hs type: exitcode-stdio-1.0 - build-depends: tasty >= 0.10 && < 1.5,+ build-depends: tasty >= 0.10 && < 1.6, tasty-hunit >= 0.10 && < 0.11, tasty-expected-failure >= 0.12 && < 0.13, -- Imports shared with the library don't need bounds@@ -359,10 +397,9 @@ benchmark benchmark import: stan-config, common, ghc2021-extensions main-is: Benchmark.hs- hs-source-dirs: bench+ hs-source-dirs: test/bench type: exitcode-stdio-1.0- build-depends: criterion >= 1.6.0.0 && < 1.7,- -- Import shared with the library don't need bounds+ build-depends: tasty-bench >= 0.3.1 && < 0.4, base, lens, linear,@@ -370,6 +407,6 @@ random, swarm, text,- containers+ containers, default-language: Haskell2010 ghc-options: -threaded
+ test/bench/Benchmark.hs view
@@ -0,0 +1,168 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}++-- |+-- SPDX-License-Identifier: BSD-3-Clause+module Main where++import Control.Lens ((&), (.~), (^.))+import Control.Monad (replicateM_)+import Control.Monad.Except (runExceptT)+import Control.Monad.State (evalStateT, execStateT)+import Data.Map qualified as M+import Swarm.Game.CESK (emptyStore, initMachine)+import Swarm.Game.Display (defaultRobotDisplay)+import Swarm.Game.Location+import Swarm.Game.Robot (TRobot, mkRobot)+import Swarm.Game.State (GameState, addTRobot, creativeMode, landscape, multiWorld)+import Swarm.Game.Step (gameTick)+import Swarm.Game.Terrain (TerrainType (DirtT))+import Swarm.Game.Universe (Cosmic (..), SubworldName (DefaultRootSubworld))+import Swarm.Game.World (WorldFun (..), newWorld)+import Swarm.Language.Context qualified as Context+import Swarm.Language.Pipeline (ProcessedTerm)+import Swarm.Language.Pipeline.QQ (tmQ)+import Swarm.Language.Syntax+import Swarm.TUI.Model (gameState)+import Swarm.TUI.Model.StateUpdate (classicGame0)+import Swarm.Util.Erasable+import Test.Tasty.Bench (Benchmark, bcompare, bench, bgroup, defaultMain, whnfAppIO)++-- | 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+-- appearance, then waits another random number of ticks, places a tree, and+-- then self-destructs.+treeProgram :: ProcessedTerm+treeProgram =+ [tmQ|+ {+ r <- random 100;+ wait (r + 300);+ appear "|";+ r <- random 100;+ wait (r + 300);+ place "tree";+ selfdestruct+ }+ |]++-- | The program of a robot that moves forward forever.+moverProgram :: ProcessedTerm+moverProgram =+ [tmQ|+ let forever : cmd unit -> cmd unit = \c. c; forever c+ in forever move+ |]++-- | The program of a robot that moves in circles forever.+circlerProgram :: ProcessedTerm+circlerProgram =+ [tmQ|+ let forever : cmd unit -> cmd unit = \c. c; forever c+ in forever (+ move;+ turn right;+ move;+ turn right;+ move;+ turn right;+ move;+ turn right;+ )+ |]++-- | The program of a robot that moves back and forth.+--+-- Each robot in a line starts a tick later, forming a wave.+-- See data/scenarios/Challenges/wave.yaml+--+-- This is used to compare the performance degradation caused+-- by using definitions and chains of ifs. Ideally there should+-- not be cost if the code is inlined and simplified. TODO: #1557+waveProgram :: Bool -> ProcessedTerm+waveProgram manualInline =+ let inlineDef = if manualInline then (1 :: Integer) else 0+ in [tmQ|+ def doN = \n. \f. if (n > 0) {f; doN (n - 1) f} {}; end;+ def crossPath =+ if ($int:inlineDef == 0) {+ doN 6 move;+ } {+ move; move; move; move; move; move;+ };+ turn back;+ wait 5;+ end;+ def go =+ crossPath;+ go;+ end;+ def start =+ pos <- whereami;+ wait $ fst pos;+ go;+ end;+ start;+ |]++-- | Initializes a robot with program prog at location loc facing north.+initRobot :: ProcessedTerm -> Location -> TRobot+initRobot prog loc = mkRobot () Nothing "" mempty (Just $ Cosmic DefaultRootSubworld loc) north defaultRobotDisplay (initMachine prog Context.empty emptyStore) [] [] False False mempty 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 :: (Location -> TRobot) -> Int -> IO GameState+mkGameState robotMaker numRobots = do+ let robots = [robotMaker (Location (fromIntegral x) 0) | x <- [0 .. numRobots - 1]]+ Right initAppState <- runExceptT classicGame0+ execStateT+ (mapM addTRobot robots)+ ( (initAppState ^. gameState)+ & creativeMode .~ True+ & landscape . multiWorld .~ M.singleton DefaultRootSubworld (newWorld (WF $ const (fromEnum DirtT, ENothing)))+ )++-- | Runs numGameTicks ticks of the game.+runGame :: Int -> GameState -> IO ()+runGame numGameTicks = evalStateT (replicateM_ numGameTicks gameTick)++main :: IO ()+main = do+ idlers <- mkGameStates idleProgram+ trees <- mkGameStates treeProgram+ circlers <- mkGameStates circlerProgram+ movers <- mkGameStates moverProgram+ wavesInlined <- mkGameStates (waveProgram True)+ wavesWithDef <- mkGameStates (waveProgram False)+ -- In theory we should force the evaluation of these game states to normal+ -- form before running the benchmarks. In practice, the first of the many+ -- criterion runs for each of these benchmarks doesn't look like an outlier.+ defaultMain+ [ bgroup+ "run 1000 game ticks"+ [ bgroup "idlers" (toBenchmarks idlers)+ , bgroup "trees" (toBenchmarks trees)+ , bgroup "circlers" (toBenchmarks circlers)+ , bgroup "movers" (toBenchmarks movers)+ , bgroup "wavesInlined" (toBenchmarks wavesInlined)+ , bgroup+ "wavesWithDef"+ ( zipWith (\i -> bcompare ("wavesInlined." <> show i)) robotNumbers $+ toBenchmarks wavesWithDef+ )+ ]+ ]+ where+ robotNumbers = [10, 20 .. 40]++ mkGameStates :: ProcessedTerm -> IO [(Int, GameState)]+ mkGameStates prog = zip robotNumbers <$> mapM (mkGameState (initRobot prog)) robotNumbers++ toBenchmarks :: [(Int, GameState)] -> [Benchmark]+ toBenchmarks gameStates =+ [ bench (show n) $ whnfAppIO (runGame 1000) gs+ | (n, gs) <- gameStates+ ]
test/integration/Main.hs view
@@ -1,7 +1,7 @@ {-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TypeApplications #-}-{-# LANGUAGE ViewPatterns #-}+{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-} -- | -- SPDX-License-Identifier: BSD-3-Clause@@ -11,7 +11,7 @@ import Control.Carrier.Lift (runM) import Control.Carrier.Throw.Either (runThrow)-import Control.Lens (Ixed (ix), to, use, view, (&), (.~), (<>~), (^.), (^..), (^?!))+import Control.Lens (Ixed (ix), at, to, use, view, (&), (.~), (<>~), (^.), (^..), (^?), (^?!)) import Control.Monad (forM_, unless, when) import Control.Monad.State (StateT (runStateT), gets) import Data.Char (isSpace)@@ -28,10 +28,11 @@ import Data.Yaml (ParseException, prettyPrintParseException) import Swarm.Doc.Gen (EditorType (..)) import Swarm.Doc.Gen qualified as DocGen+import Swarm.Game.Achievement.Definitions (GameplayAchievement (..)) import Swarm.Game.CESK (emptyStore, getTickNumber, initMachine) import Swarm.Game.Entity (EntityMap, lookupByName) import Swarm.Game.Failure (SystemFailure)-import Swarm.Game.Robot (LogEntry, defReqs, equippedDevices, leText, machine, robotContext, robotLog, waitingUntil)+import Swarm.Game.Robot (activityCounts, commandsHistogram, defReqs, equippedDevices, lifetimeStepCount, machine, robotContext, robotLog, systemRobot, tangibleCommandCount, waitingUntil) import Swarm.Game.Scenario (Scenario) import Swarm.Game.State ( GameState,@@ -39,8 +40,13 @@ WinStatus (Won), activeRobots, baseRobot,+ discovery,+ gameAchievements,+ messageInfo, messageQueue,+ notificationsContent, robotMap,+ temporal, ticks, waitingRobots, winCondition,@@ -51,7 +57,17 @@ import Swarm.Language.Context qualified as Ctx import Swarm.Language.Pipeline (ProcessedTerm (..), processTerm) import Swarm.Language.Pretty (prettyString)-import Swarm.TUI.Model (RuntimeState, defaultAppOpts, gameState, stdEntityMap, userScenario, worlds)+import Swarm.Log+import Swarm.TUI.Model (+ RuntimeState,+ defaultAppOpts,+ eventLog,+ gameState,+ runtimeState,+ stdEntityMap,+ userScenario,+ worlds,+ ) import Swarm.TUI.Model.StateUpdate (constructAppState, initPersistentState) import Swarm.TUI.Model.UI (UIState) import Swarm.Util (acquireAllWithExt)@@ -59,7 +75,7 @@ import System.FilePath.Posix (splitDirectories) import System.Timeout (timeout) import Test.Tasty (TestTree, defaultMain, testGroup)-import Test.Tasty.HUnit (Assertion, assertBool, assertFailure, testCase)+import Test.Tasty.HUnit (Assertion, assertBool, assertEqual, assertFailure, testCase) import Witch (into) isUnparseableTest :: (FilePath, String) -> Bool@@ -75,17 +91,33 @@ out <- runM . runThrow @SystemFailure $ initPersistentState defaultAppOpts either (assertFailure . prettyString) return out let em = rs ^. stdEntityMap+ let rs' = rs & eventLog .~ mempty defaultMain $ testGroup "Tests"- [ exampleTests examplePaths+ [ testNoLoadingErrors rs+ , exampleTests examplePaths , exampleTests scenarioPrograms , scenarioParseTests em (rs ^. worlds) parseableScenarios , scenarioParseInvalidTests em (rs ^. worlds) unparseableScenarios- , testScenarioSolutions rs ui+ , testScenarioSolutions rs' ui , testEditorFiles ] +testNoLoadingErrors :: RuntimeState -> TestTree+testNoLoadingErrors r =+ testCase "Test runtime log does not contain errors" (checkNoRuntimeErrors r)++checkNoRuntimeErrors :: RuntimeState -> IO ()+checkNoRuntimeErrors r =+ forM_ (r ^. eventLog . notificationsContent) $ \e ->+ when (isError e) $+ assertFailure $+ show (e ^. leSeverity) <> " was produced during loading: " <> T.unpack (e ^. leText)++isError :: LogEntry -> Bool+isError = (>= Warning) . view leSeverity+ exampleTests :: [(FilePath, String)] -> TestTree exampleTests inputs = testGroup "Test example" (map exampleTest inputs) @@ -172,6 +204,7 @@ , testTutorialSolution Default "Tutorials/require" , testTutorialSolution (Sec 3) "Tutorials/requireinv" , testTutorialSolution Default "Tutorials/conditionals"+ , testTutorialSolution Default "Tutorials/world101" , testTutorialSolution (Sec 5) "Tutorials/farming" ] , testGroup@@ -181,10 +214,12 @@ , testSolution Default "Challenges/maypole" , testSolution (Sec 5) "Challenges/2048" , testSolution (Sec 3) "Challenges/word-search"- , testSolution (Sec 5) "Challenges/bridge-building"- , testSolution (Sec 3) "Challenges/ice-cream"+ , testSolution (Sec 10) "Challenges/bridge-building"+ , testSolution (Sec 5) "Challenges/ice-cream"+ , testSolution (Sec 10) "Challenges/combo-lock"+ , testSolution (Sec 15) "Challenges/wave" , testSolution (Sec 3) "Challenges/arbitrage"- , testSolution (Sec 5) "Challenges/gopher"+ , testSolution (Sec 10) "Challenges/gopher" , testSolution (Sec 5) "Challenges/hackman" , testSolution (Sec 5) "Challenges/blender" , testSolution (Sec 10) "Challenges/hanoi"@@ -201,7 +236,7 @@ , testGroup "Ranching" [ testSolution Default "Challenges/Ranching/capture"- , testSolution (Sec 5) "Challenges/Ranching/powerset"+ , testSolution (Sec 10) "Challenges/Ranching/powerset" , testSolution (Sec 30) "Challenges/Ranching/gated-paddock" ] , testGroup@@ -217,12 +252,19 @@ ] ] , testGroup+ "Achievements"+ [ testSolution' Default "Testing/Achievements/RobotIntoWater" CheckForBadErrors $ \g ->+ assertBool+ "Did not get RobotIntoWater achievement!"+ (isJust $ g ^? discovery . gameAchievements . at RobotIntoWater)+ ]+ , testGroup "Regression tests" [ testSolution Default "Testing/394-build-drill" , testSolution Default "Testing/373-drill" , testSolution Default "Testing/428-drowning-destroy" , testSolution' Default "Testing/475-wait-one" CheckForBadErrors $ \g -> do- let t = g ^. ticks+ let t = g ^. temporal . ticks r1Waits = g ^?! robotMap . ix 1 . to waitingUntil active = IS.member 1 $ g ^. activeRobots waiting = elem 1 . concat . M.elems $ g ^. waitingRobots@@ -252,7 +294,7 @@ , testSolution Default "Testing/479-atomic-race" , testSolution (Sec 5) "Testing/479-atomic" , testSolution Default "Testing/555-teleport-location"- , testSolution Default "Testing/562-lodestone"+ , testSolution (Sec 2) "Testing/562-lodestone" , testSolution Default "Testing/378-objectives" , testSolution Default "Testing/684-swap" , testSolution Default "Testing/699-movement-fail/699-move-blocked"@@ -263,7 +305,7 @@ , testSolution Default "Testing/955-heading" , testSolution' Default "Testing/397-wrong-missing" CheckForBadErrors $ \g -> do let msgs =- (g ^. messageQueue . to seqToTexts)+ (g ^. messageInfo . messageQueue . to seqToTexts) <> (g ^.. robotMap . traverse . robotLog . to seqToTexts . traverse) assertBool "Should be some messages" (not (null msgs))@@ -290,8 +332,10 @@ , testSolution Default "Testing/144-subworlds/basic-subworld" , testSolution Default "Testing/144-subworlds/subworld-mapped-robots" , testSolution Default "Testing/144-subworlds/subworld-located-robots"+ , testSolution Default "Testing/1355-combustion" , testSolution Default "Testing/1379-single-world-portal-reorientation" , testSolution Default "Testing/1399-backup-command"+ , testSolution Default "Testing/1536-custom-unwalkable-entities" , testGroup -- Note that the description of the classic world in -- data/worlds/classic.yaml (automatically tested to some@@ -303,7 +347,41 @@ , testSolution Default "Testing/1320-world-DSL/erase" , testSolution Default "Testing/1320-world-DSL/override" ]+ , testGroup+ "Pathfinding (#836)"+ [ testSolution Default "Testing/836-pathfinding/836-path-exists-find-entity"+ , testSolution Default "Testing/836-pathfinding/836-path-exists-find-location"+ , testSolution Default "Testing/836-pathfinding/836-path-exists-find-entity-unwalkable"+ , testSolution Default "Testing/836-pathfinding/836-path-exists-distance-limit-unreachable"+ , testSolution Default "Testing/836-pathfinding/836-path-exists-distance-limit-unreachable"+ , testSolution Default "Testing/836-pathfinding/836-no-path-exists1"+ , testSolution (Sec 10) "Testing/836-pathfinding/836-no-path-exists2"+ , testSolution (Sec 3) "Testing/836-pathfinding/836-automatic-waypoint-navigation.yaml"+ ]+ , testGroup+ "Ping (#1535)"+ [ testSolution Default "Testing/1535-ping/1535-in-range"+ , testSolution Default "Testing/1535-ping/1535-out-of-range"+ ] ]+ , testSolution' Default "Testing/1430-built-robot-ownership" CheckForBadErrors $ \g -> do+ let r2 = g ^. robotMap . at 2+ let r3 = g ^. robotMap . at 3+ assertBool "The second built robot should be a system robot like it's parent." $+ maybe False (view systemRobot) r2+ assertBool "The third built robot should be a normal robot like base." $+ maybe False (not . view systemRobot) r3+ , testSolution' Default "Testing/1341-command-count" CheckForBadErrors $ \g -> case g ^. robotMap . at 0 of+ Nothing -> assertFailure "No base bot!"+ Just base -> do+ let counters = base ^. activityCounts+ -- NOTE: The values of 7 and 10 for "tangible" and "total" command counts+ -- make sense from the test program and match the F2 screen upon winning the scenario.+ -- However, the F2 dialog actually shows 64 for the step count. This test was+ -- hardcoded to 62 just to make it pass.+ assertEqual "Incorrect tangible command count." 7 $ view tangibleCommandCount counters+ assertEqual "Incorrect command count." 10 $ sum . M.elems $ view commandsHistogram counters+ assertEqual "Incorrect step count." 62 $ view lifetimeStepCount counters ] where -- expectFailIf :: Bool -> String -> TestTree -> TestTree@@ -317,11 +395,12 @@ out <- runM . runThrow @SystemFailure $ constructAppState rs ui $ defaultAppOpts {userScenario = Just p} case out of Left err -> assertFailure $ prettyString err- Right (view gameState -> gs) -> case gs ^. winSolution of+ Right appState -> case appState ^. gameState . winSolution of Nothing -> assertFailure "No solution to test!" Just sol@(ProcessedTerm _ _ reqCtx) -> do+ when (shouldCheckBadErrors == CheckForBadErrors) (checkNoRuntimeErrors $ appState ^. runtimeState) let gs' =- gs+ (appState ^. gameState) -- 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).@@ -362,7 +441,7 @@ concatMap (\r -> filter isBad (seqToTexts $ r ^. robotLog)) (g ^. robotMap)- <> filter isBad (seqToTexts $ g ^. messageQueue)+ <> filter isBad (seqToTexts $ g ^. messageInfo . messageQueue) where isBad m = "Fatal error:" `T.isInfixOf` m || "swarm/issues" `T.isInfixOf` m @@ -393,10 +472,17 @@ , testTextInEmacs "commands" DocGen.keywordsCommands , testTextInEmacs "directions" DocGen.keywordsDirections ]+ , testGroup+ "Vim"+ [ testTextInVim "builtin" DocGen.builtinFunctionList+ , testTextInVim "commands" DocGen.keywordsCommands+ , testTextInVim "directions" DocGen.keywordsDirections+ ] ] where testTextInVSCode name tf = testTextInFile False name (tf VSCode) "editors/vscode/syntaxes/swarm.tmLanguage.json" testTextInEmacs name tf = testTextInFile True name (tf Emacs) "editors/emacs/swarm-mode.el"+ testTextInVim name tf = testTextInFile True name (tf Vim) "editors/vim/swarm.vim" testTextInFile :: Bool -> String -> Text -> FilePath -> TestTree testTextInFile whitespace name t fp = testCase name $ do let removeLW' = T.unlines . map (T.dropWhile isSpace) . T.lines
test/unit/TestLanguagePipeline.hs view
@@ -11,15 +11,16 @@ 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.Module (Module (..))+import Swarm.Language.Parse (readTerm) import Swarm.Language.Parse.QQ (tyQ) import Swarm.Language.Pipeline (ProcessedTerm (..), processTerm) import Swarm.Language.Pipeline.QQ (tmQ)+import Swarm.Language.Pretty (prettyText) import Swarm.Language.Syntax import Swarm.Language.Typecheck (isSimpleUType) import Swarm.Language.Types@@ -161,8 +162,8 @@ ] , testGroup "json encoding"- [ testCase "simple expr" (roundTrip "42 + 43")- , testCase "module def" (roundTrip "def x = 41 end; def y = 42 end")+ [ testCase "simple expr" (roundTripTerm "42 + 43")+ , testCase "module def" (roundTripTerm "def x = 41 end;\ndef y = 42 end") ] , testGroup "atomic - #479"@@ -180,19 +181,19 @@ "atomic move+move" ( process "atomic (move; move)"- "1:8: Invalid atomic block: block could take too many ticks (2): move; move"+ "1:8: Invalid atomic block: block could take too many ticks (2): `move; move`" ) , testCase "atomic lambda" ( process "atomic ((\\c. c;c) move)"- "1:9: Invalid atomic block: def, let, and lambda are not allowed: \\c. c; c"+ "1:9: Invalid atomic block: def, let, and lambda are not allowed: `\\c. c; c`" ) , testCase "atomic non-simple" ( process "def dup = \\c. c; c end; atomic (dup (dup move))"- "1:33: Invalid atomic block: reference to variable with non-simple type ∀ a. cmd a -> cmd a: dup"+ "1:33: Invalid atomic block: reference to variable with non-simple type ∀ a. cmd a -> cmd a: `dup`" ) , testCase "atomic nested"@@ -204,25 +205,25 @@ "atomic wait" ( process "atomic (wait 1)"- "1:9: Invalid atomic block: commands that can take multiple ticks to execute are not allowed: wait"+ "1:9: Invalid atomic block: commands that can take multiple ticks to execute are not allowed: `wait`" ) , testCase "atomic make" ( process "atomic (make \"PhD thesis\")"- "1:9: Invalid atomic block: commands that can take multiple ticks to execute are not allowed: make"+ "1:9: Invalid atomic block: commands that can take multiple ticks to execute are not allowed: `make`" ) , testCase "atomic drill" ( process "atomic (drill forward)"- "1:9: Invalid atomic block: commands that can take multiple ticks to execute are not allowed: drill"+ "1:9: Invalid atomic block: commands that can take multiple ticks to execute are not allowed: `drill`" ) , testCase "atomic salvage" ( process "atomic (salvage)"- "1:8: Invalid atomic block: commands that can take multiple ticks to execute are not allowed: salvage"+ "1:8: Invalid atomic block: commands that can take multiple ticks to execute are not allowed: `salvage`" ) ] , testGroup@@ -381,25 +382,43 @@ "1:5: Type mismatch:\n From context, expected `\\x. \\y. 3` to have type `int`,\n but it is actually a function\n" ) ]+ , testGroup+ "generalize top-level binds #351 #1501"+ [ testCase+ "top-level polymorphic bind is OK"+ (valid "r <- return (\\x.x)")+ , testCase+ "top-level bind is polymorphic"+ (valid "f <- return (\\x.x); return (f 3, f \"hi\")")+ , testCase+ "local bind is polymorphic"+ (valid "def foo : cmd (int * text) = f <- return (\\x.x); return (f 3, f \"hi\") end")+ ] ] where valid = flip process "" - 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- Right x -> x- term = fromMaybe (error "") $ fromRight (error "") $ processTerm txt- process :: Text -> Text -> Assertion process code expect = case processTerm code of Left e | not (T.null expect) && expect `T.isPrefixOf` e -> pure ()- | otherwise -> error $ "Unexpected failure: " <> show e+ | otherwise ->+ error $+ "Unexpected failure:\n\n " <> show e <> "\n\nExpected:\n\n " <> show expect <> "\n" Right _ | expect == "" -> pure () | otherwise -> error "Unexpected success" getSyntax :: ProcessedTerm -> Syntax' Polytype getSyntax (ProcessedTerm (Module s _) _ _) = s++-- | Check round tripping of term from and to text, then test ToJSON/FromJSON.+roundTripTerm :: Text -> Assertion+roundTripTerm txt = do+ assertEqual "roundtrip (readTerm -> prettyText)" txt (prettyText term)+ assertEqual "roundtrip (ToJSON -> FromJSON)" 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+ Right x -> x+ term = fromMaybe (error "empty document") $ either (error . T.unpack) id $ readTerm txt
test/unit/TestNotification.hs view
@@ -12,6 +12,7 @@ import Swarm.Game.CESK (TickNumber (..)) import Swarm.Game.Robot import Swarm.Game.State+import Swarm.Log import Test.Tasty import Test.Tasty.HUnit import TestUtil@@ -21,22 +22,22 @@ testGroup "Notifications" [ testCase "notifications at start" $ do- assertBool "There should be no messages in queue" (null (gs ^. messageQueue))+ assertBool "There should be no messages in queue" (null (gs ^. messageInfo . messageQueue)) assertNew gs 0 "messages at game start" messageNotifications- assertNew gs 0 "recipes at game start" availableRecipes- assertNew gs 0 "commands at game start" availableCommands+ assertNew gs 0 "recipes at game start" (discovery . availableRecipes)+ assertNew gs 0 "commands at game start" (discovery . availableCommands) , testCase "new message after say" $ do gs' <- goodPlay "say \"Hello world!\""- assertBool "There should be one message in queue" (length (gs' ^. messageQueue) == 1)+ assertBool "There should be one message in queue" (length (gs' ^. messageInfo . messageQueue) == 1) assertNew gs' 1 "message" messageNotifications , testCase "two new messages after say twice" $ do gs' <- goodPlay "say \"Hello!\"; say \"Goodbye!\""- assertBool "There should be two messages in queue" (length (gs' ^. messageQueue) == 2)+ assertBool "There should be two messages in queue" (length (gs' ^. messageInfo . messageQueue) == 2) assertNew gs' 2 "messages" messageNotifications , testCase "one new message and one old message" $ do gs' <- goodPlay "say \"Hello!\"; say \"Goodbye!\"" assertEqual "There should be two messages in queue" [TickNumber 0, TickNumber 1] (view leTime <$> gs' ^. messageNotifications . notificationsContent)- assertNew (gs' & lastSeenMessageTime .~ TickNumber 0) 1 "message" messageNotifications+ assertNew (gs' & messageInfo . lastSeenMessageTime .~ TickNumber 0) 1 "message" messageNotifications , testCase "new message after log" $ do gs' <- goodPlay "create \"logger\"; equip \"logger\"; log \"Hello world!\"" let r = gs' ^?! robotMap . ix (-1)@@ -46,7 +47,7 @@ assertNew gs' 1 "message" messageNotifications , testCase "new message after build say" $ do gs' <- goodPlay "build {say \"Hello world!\"}; turn back; turn back;"- assertBool "There should be one message in queue" (length (gs' ^. messageQueue) == 1)+ assertBool "There should be one message in queue" (length (gs' ^. messageInfo . messageQueue) == 1) assertNew gs' 1 "message" messageNotifications , testCase "no new message after build log" $ do gs' <- goodPlay "build {log \"Hello world!\"}; turn back; turn back;"
test/unit/TestPretty.hs view
@@ -95,6 +95,18 @@ ( equalPretty "1 : int" $ TAnnotate (TInt 1) (Forall [] TyInt) )+ , testCase+ "lambda precedence (#1468)"+ ( equalPretty "\\m. case m (\\x. x + 1) (\\y. y * 2)" $+ TLam+ "m"+ Nothing+ ( TConst Case+ :$: STerm (TVar "m")+ :$: STerm (TLam "x" Nothing (mkOp' Add (TVar "x") (TInt 1)))+ :$: STerm (TLam "y" Nothing (mkOp' Mul (TVar "y") (TInt 2)))+ )+ ) ] where equalPretty :: String -> Term -> Assertion
test/unit/TestUtil.hs view
@@ -44,7 +44,7 @@ orderResult ((res, rr), rg) = (rg, rr, res) runCESK :: Int -> CESK -> StateT Robot (StateT GameState IO) (Either Text (Value, Int))-runCESK _ (Up exn _ []) = Left . flip formatExn exn <$> lift (use entityMap)+runCESK _ (Up exn _ []) = Left . flip formatExn exn <$> lift (use $ landscape . entityMap) runCESK !steps cesk = case finalValue cesk of Just (v, _) -> return (Right (v, steps)) Nothing -> stepCESK cesk >>= runCESK (steps + 1)