diff --git a/src/swarm-engine/Swarm/Game/ScenarioInfo.hs b/src/swarm-engine/Swarm/Game/ScenarioInfo.hs index 514fce85e..107dbe85a 100644 --- a/src/swarm-engine/Swarm/Game/ScenarioInfo.hs +++ b/src/swarm-engine/Swarm/Game/ScenarioInfo.hs @@ -73,7 +73,6 @@ import Witch (into) -- | A scenario item is either a specific scenario, or a collection of -- scenarios (/e.g./ the scenarios contained in a subdirectory). data ScenarioItem = SISingle ScenarioInfoPair | SICollection Text ScenarioCollection - deriving (Show) -- | Retrieve the name of a scenario item. scenarioItemName :: ScenarioItem -> Text @@ -89,7 +88,6 @@ data ScenarioCollection = SC { scOrder :: Maybe [FilePath] , scMap :: Map FilePath ScenarioItem } - deriving (Show) -- | Access and modify 'ScenarioItem's in collection based on their path. scenarioItemByPath :: FilePath -> Traversal' ScenarioCollection ScenarioItem @@ -114,7 +112,7 @@ tutorialsDirname = "Tutorials" getTutorials :: ScenarioCollection -> ScenarioCollection getTutorials sc = case M.lookup tutorialsDirname (scMap sc) of Just (SICollection _ c) -> c - _ -> error $ "No tutorials exist: " ++ show sc + _ -> error "No tutorials exist" -- | Canonicalize a scenario path, making it usable as a unique key. normalizeScenarioPath :: diff --git a/src/swarm-engine/Swarm/Game/State/Initialize.hs b/src/swarm-engine/Swarm/Game/State/Initialize.hs index 8db37b109..f318bee25 100644 --- a/src/swarm-engine/Swarm/Game/State/Initialize.hs +++ b/src/swarm-engine/Swarm/Game/State/Initialize.hs @@ -12,9 +12,8 @@ module Swarm.Game.State.Initialize ( import Control.Arrow (Arrow ((&&&))) import Control.Carrier.State.Lazy qualified as Fused import Control.Effect.Lens (view) -import Control.Effect.Lift (Has) -import Control.Effect.State (State) -import Control.Lens hiding (Const, use, uses, view, (%=), (+=), (.=), (<+=), (<<.=)) +import Control.Lens hiding (view) +import Data.Hashable (Hashable) import Data.IntMap qualified as IM import Data.List (partition) import Data.List.NonEmpty (NonEmpty) @@ -24,6 +23,7 @@ import Data.Map qualified as M import Data.Maybe (isNothing) import Data.Set qualified as S import Data.Text (Text) +import Data.Tuple.Extra (dupe) import Swarm.Game.CESK (finalValue, initMachine) import Swarm.Game.Device (getCapabilitySet, getMap) import Swarm.Game.Entity @@ -38,12 +38,10 @@ import Swarm.Game.Robot.Concrete import Swarm.Game.Scenario import Swarm.Game.Scenario.Objective (initCompletion) import Swarm.Game.Scenario.Status -import Swarm.Game.Scenario.Topography.Cell (Cell, cellToEntity) import Swarm.Game.Scenario.Topography.Structure.Recognition import Swarm.Game.Scenario.Topography.Structure.Recognition.Log import Swarm.Game.Scenario.Topography.Structure.Recognition.Precompute import Swarm.Game.Scenario.Topography.Structure.Recognition.Registry (emptyFoundStructures) -import Swarm.Game.Scenario.Topography.Structure.Recognition.Static import Swarm.Game.Scenario.Topography.Structure.Recognition.Type import Swarm.Game.State import Swarm.Game.State.Landscape (mkLandscape) @@ -78,14 +76,19 @@ pureScenarioToGameState :: GameState pureScenarioToGameState scenario theSeed now toRun gsc = preliminaryGameState - & discovery . structureRecognition .~ recognizer + & discovery . structureRecognition .~ recognition where sLandscape = scenario ^. scenarioLandscape - recognizer = - runIdentity $ - Fused.evalState preliminaryGameState $ - mkRecognizer (sLandscape ^. scenarioStructures) + -- It may be possible at some point for the game seed to affect whether + -- initially-placed structures remain intact, by way of random placements. + -- Therefore we run this at 'GameState' initialization time, rather than + -- 'Scenario' parse time. + recognition = + runIdentity + . Fused.evalState preliminaryGameState + . adaptGameState + $ initializeRecognition mtlEntityAt (sLandscape ^. scenarioStructures) gs = initGameState gsc preliminaryGameState = @@ -184,24 +187,23 @@ pureScenarioToGameState scenario theSeed now toRun gsc = -- we don't actually have to "search" for these structures since we are -- explicitly given their location; we only need to validate that each -- structure remains intact given other, potentially overlapping static placements. -mkRecognizer :: - (Has (State GameState) sig m) => - StaticStructureInfo Cell -> - m (StructureRecognizer (Maybe Cell) Entity) -mkRecognizer structInfo@(StaticStructureInfo structDefs _) = do +initializeRecognition :: + (Monad s, Hashable a, Eq b) => + GenericEntLocator s a -> + StaticStructureInfo b a -> + s (RecognitionState b a) +initializeRecognition entLoader structInfo = do foundIntact <- mapM checkIntactness allPlaced let fs = populateStaticFoundStructures . map fst . filter (null . snd) $ foundIntact - return - $ StructureRecognizer - (mkAutomatons cellToEntity structDefs) - $ RecognitionState + return $ + RecognitionState fs [IntactStaticPlacement $ map mkLogEntry foundIntact] where - checkIntactness = sequenceA . (id &&& adaptGameState . ensureStructureIntact emptyFoundStructures mtlEntityAt) + checkIntactness = traverse (ensureStructureIntact emptyFoundStructures entLoader) . dupe - allPlaced = lookupStaticPlacements cellToEntity structInfo + allPlaced = lookupStaticPlacements structInfo mkLogEntry (x, intact) = IntactPlacementLog intact diff --git a/src/swarm-engine/Swarm/Game/State/Substate.hs b/src/swarm-engine/Swarm/Game/State/Substate.hs index ba98b63ae..ee105b423 100644 --- a/src/swarm-engine/Swarm/Game/State/Substate.hs +++ b/src/swarm-engine/Swarm/Game/State/Substate.hs @@ -104,12 +104,12 @@ import Swarm.Game.Recipe ( outRecipeMap, ) import Swarm.Game.Robot -import Swarm.Game.Scenario (GameStateInputs (..)) +import Swarm.Game.Scenario (GameStateInputs (..), RecognizableStructureContent) import Swarm.Game.Scenario.Objective import Swarm.Game.Scenario.Topography.Cell (Cell) +import Swarm.Game.Scenario.Topography.Grid import Swarm.Game.Scenario.Topography.Structure.Recognition import Swarm.Game.Scenario.Topography.Structure.Recognition.Registry (emptyFoundStructures) -import Swarm.Game.Scenario.Topography.Structure.Recognition.Type (RecognizerAutomatons (..)) import Swarm.Game.State.Config import Swarm.Game.Tick (TickNumber (..)) import Swarm.Game.World.Gen (Seed) @@ -342,7 +342,7 @@ data Discovery = Discovery , _availableCommands :: Notifications Const , _knownEntities :: S.Set EntityName , _gameAchievements :: Map GameplayAchievement Attainment - , _structureRecognition :: StructureRecognizer (Maybe Cell) Entity + , _structureRecognition :: RecognitionState (NonEmptyGrid (Maybe Cell)) Entity , _tagMembers :: Map Text (NonEmpty EntityName) } @@ -365,7 +365,7 @@ knownEntities :: Lens' Discovery (S.Set EntityName) gameAchievements :: Lens' Discovery (Map GameplayAchievement Attainment) -- | Recognizer for robot-constructed structures -structureRecognition :: Lens' Discovery (StructureRecognizer (Maybe Cell) Entity) +structureRecognition :: Lens' Discovery (RecognitionState RecognizableStructureContent Entity) -- | Map from tags to entities that possess that tag tagMembers :: Lens' Discovery (Map Text (NonEmpty EntityName)) @@ -446,10 +446,7 @@ initDiscovery = , -- This does not need to be initialized with anything, -- since the master list of achievements is stored in UIState _gameAchievements = mempty - , _structureRecognition = - StructureRecognizer - (RecognizerAutomatons mempty mempty) - (RecognitionState emptyFoundStructures []) + , _structureRecognition = RecognitionState emptyFoundStructures [] , _tagMembers = mempty } diff --git a/src/swarm-engine/Swarm/Game/Step/Const.hs b/src/swarm-engine/Swarm/Game/Step/Const.hs index c62f04019..37b97f952 100644 --- a/src/swarm-engine/Swarm/Game/Step/Const.hs +++ b/src/swarm-engine/Swarm/Game/Step/Const.hs @@ -59,12 +59,12 @@ import Swarm.Game.Robot import Swarm.Game.Robot.Activity import Swarm.Game.Robot.Concrete import Swarm.Game.Robot.Walk (emptyExceptions) -import Swarm.Game.Scenario.Topography.Area (getAreaDimensions, getNEGridDimensions, rectHeight) +import Swarm.Game.Scenario.Topography.Area (getNEGridDimensions, rectHeight) import Swarm.Game.Scenario.Topography.Navigation.Portal (Navigation (..)) import Swarm.Game.Scenario.Topography.Navigation.Util import Swarm.Game.Scenario.Topography.Navigation.Waypoint (WaypointName (..)) import Swarm.Game.Scenario.Topography.Structure.Named (StructureName (..)) -import Swarm.Game.Scenario.Topography.Structure.Recognition (automatons, foundStructures, recognitionState) +import Swarm.Game.Scenario.Topography.Structure.Recognition (foundStructures) import Swarm.Game.Scenario.Topography.Structure.Recognition.Registry (foundByName) import Swarm.Game.Scenario.Topography.Structure.Recognition.Type import Swarm.Game.State @@ -549,26 +549,26 @@ execConst runChildProg c vs s k = do _ -> badConst Structure -> case vs of [VText name, VInt idx] -> do - registry <- use $ discovery . structureRecognition . recognitionState . foundStructures + registry <- use $ discovery . structureRecognition . foundStructures let maybeFoundStructures = M.lookup (StructureName name) $ foundByName registry mkOutput mapNE = (NE.length xs, bottomLeftCorner) where xs = NEM.toList mapNE (pos, struc) = indexWrapNonEmpty xs idx topLeftCorner = pos ^. planar - offsetHeight = V2 0 $ negate (rectHeight (getNEGridDimensions $ entityGrid struc) - 1) + offsetHeight = V2 0 $ negate (rectHeight (getNEGridDimensions $ extractedGrid $ entityGrid struc) - 1) bottomLeftCorner :: Location bottomLeftCorner = topLeftCorner .+^ offsetHeight return $ mkReturn $ mkOutput <$> maybeFoundStructures _ -> badConst Floorplan -> case vs of [VText name] -> do - structureTemplates <- use $ discovery . structureRecognition . automatons . originalStructureDefinitions + structureTemplates <- use $ landscape . recognizerAutomatons . originalStructureDefinitions let maybeStructure = M.lookup (StructureName name) structureTemplates structureDef <- maybeStructure `isJustOr` cmdExn Floorplan (pure $ T.unwords ["Unknown structure", quote name]) - return . mkReturn . getAreaDimensions $ entityProcessedGrid structureDef + return . mkReturn . getNEGridDimensions $ entityProcessedGrid structureDef _ -> badConst HasTag -> case vs of [VText eName, VText tName] -> do diff --git a/src/swarm-engine/Swarm/Game/Step/Util.hs b/src/swarm-engine/Swarm/Game/Step/Util.hs index e0acc276d..706301eb3 100644 --- a/src/swarm-engine/Swarm/Game/Step/Util.hs +++ b/src/swarm-engine/Swarm/Game/Step/Util.hs @@ -30,6 +30,7 @@ import Swarm.Game.Location import Swarm.Game.Robot import Swarm.Game.Scenario.Topography.Structure.Recognition.Tracking qualified as SRT import Swarm.Game.State +import Swarm.Game.State.Landscape (recognizerAutomatons) import Swarm.Game.State.Robot import Swarm.Game.State.Substate import Swarm.Game.Step.Path.Cache @@ -87,9 +88,10 @@ updateEntityAt cLoc@(Cosmic subworldName loc) upd = do myID <- use robotID zoomRobots $ wakeWatchingRobots myID currentTick cLoc - oldRecognizer <- use $ discovery . structureRecognition - newRecognizer <- adaptGameState $ SRT.entityModified mtlEntityAt modType cLoc oldRecognizer - discovery . structureRecognition .= newRecognizer + structureRecognizer <- use $ landscape . recognizerAutomatons + oldRecognition <- use $ discovery . structureRecognition + newRecognition <- adaptGameState $ SRT.entityModified mtlEntityAt modType cLoc structureRecognizer oldRecognition + discovery . structureRecognition .= newRecognition pcr <- use $ pathCaching . pathCachingRobots mapM_ (revalidatePathCache cLoc modType) $ IM.toList pcr diff --git a/src/swarm-scenario/Swarm/Game/Scenario.hs b/src/swarm-scenario/Swarm/Game/Scenario.hs index da955f658..fb731ce5c 100644 --- a/src/swarm-scenario/Swarm/Game/Scenario.hs +++ b/src/swarm-scenario/Swarm/Game/Scenario.hs @@ -17,8 +17,8 @@ module Swarm.Game.Scenario ( Scenario (..), ScenarioLandscape (..), ScenarioMetadata (ScenarioMetadata), + RecognizableStructureContent, staticPlacements, - structureDefs, -- ** Fields scenarioMetadata, @@ -86,14 +86,16 @@ import Swarm.Game.Scenario.Objective.Validation import Swarm.Game.Scenario.RobotLookup import Swarm.Game.Scenario.Style import Swarm.Game.Scenario.Topography.Cell +import Swarm.Game.Scenario.Topography.Grid import Swarm.Game.Scenario.Topography.Navigation.Portal import Swarm.Game.Scenario.Topography.Navigation.Waypoint (Parentage (..)) import Swarm.Game.Scenario.Topography.Structure qualified as Structure import Swarm.Game.Scenario.Topography.Structure.Assembly qualified as Assembly import Swarm.Game.Scenario.Topography.Structure.Named qualified as Structure import Swarm.Game.Scenario.Topography.Structure.Overlay -import Swarm.Game.Scenario.Topography.Structure.Recognition.Static -import Swarm.Game.Scenario.Topography.Structure.Recognition.Symmetry +import Swarm.Game.Scenario.Topography.Structure.Recognition.Precompute +import Swarm.Game.Scenario.Topography.Structure.Recognition.Symmetry (renderRedundancy) +import Swarm.Game.Scenario.Topography.Structure.Recognition.Type import Swarm.Game.Scenario.Topography.WorldDescription import Swarm.Game.Terrain import Swarm.Game.Universe @@ -181,6 +183,8 @@ scenarioSolution :: Lens' ScenarioOperation (Maybe TSyntax) -- take during a single tick. scenarioStepsPerTick :: Lens' ScenarioOperation (Maybe Int) +type RecognizableStructureContent = NonEmptyGrid (Maybe Cell) + -- | All cosmetic and structural content of the scenario. data ScenarioLandscape = ScenarioLandscape { _scenarioSeed :: Maybe Int @@ -190,10 +194,9 @@ data ScenarioLandscape = ScenarioLandscape , _scenarioKnown :: Set EntityName , _scenarioWorlds :: NonEmpty WorldDescription , _scenarioNavigation :: Navigation (M.Map SubworldName) Location - , _scenarioStructures :: StaticStructureInfo Cell + , _scenarioStructures :: StaticStructureInfo RecognizableStructureContent Entity , _scenarioRobots :: [TRobot] } - deriving (Show) makeLensesNoSigs ''ScenarioLandscape @@ -220,7 +223,7 @@ scenarioKnown :: Lens' ScenarioLandscape (Set EntityName) scenarioWorlds :: Lens' ScenarioLandscape (NonEmpty WorldDescription) -- | Information required for structure recognition -scenarioStructures :: Lens' ScenarioLandscape (StaticStructureInfo Cell) +scenarioStructures :: Lens' ScenarioLandscape (StaticStructureInfo RecognizableStructureContent Entity) -- | Waypoints and inter-world portals scenarioNavigation :: Lens' ScenarioLandscape (Navigation (M.Map SubworldName) Location) @@ -236,7 +239,6 @@ data Scenario = Scenario , _scenarioOperation :: ScenarioOperation , _scenarioLandscape :: ScenarioLandscape } - deriving (Show) makeLensesNoSigs ''Scenario @@ -355,10 +357,15 @@ instance FromJSONE ScenarioInputs Scenario where namedGrids = map stuffGrid mergedStructures recognizableGrids = filter Structure.isRecognizable namedGrids - symmetryAnnotatedGrids <- mapM checkSymmetry recognizableGrids + -- We exclude empty grids from the recognition engine. + nonEmptyRecognizableGrids = mapMaybe (traverse getNonEmptyGrid) recognizableGrids + + myAutomatons <- + either (fail . T.unpack . renderRedundancy) return $ + mkAutomatons (fmap cellToEntity) nonEmptyRecognizableGrids let structureInfo = - StaticStructureInfo symmetryAnnotatedGrids + StaticStructureInfo myAutomatons . M.fromList . NE.toList $ NE.map (worldName &&& placedStructures) allWorlds diff --git a/src/swarm-scenario/Swarm/Game/State/Landscape.hs b/src/swarm-scenario/Swarm/Game/State/Landscape.hs index bb1b46e77..aa09020d1 100644 --- a/src/swarm-scenario/Swarm/Game/State/Landscape.hs +++ b/src/swarm-scenario/Swarm/Game/State/Landscape.hs @@ -13,6 +13,7 @@ module Swarm.Game.State.Landscape ( multiWorld, worldScrollable, terrainAndEntities, + recognizerAutomatons, -- ** Utilities initLandscape, @@ -45,6 +46,7 @@ import Swarm.Game.Scenario.Topography.Cell import Swarm.Game.Scenario.Topography.Grid import Swarm.Game.Scenario.Topography.Navigation.Portal (Navigation (..)) import Swarm.Game.Scenario.Topography.Structure.Overlay +import Swarm.Game.Scenario.Topography.Structure.Recognition.Type import Swarm.Game.Scenario.Topography.WorldDescription import Swarm.Game.State.Config import Swarm.Game.Terrain (TerrainType (..), terrainIndexByName) @@ -63,6 +65,7 @@ data Landscape = Landscape { _worldNavigation :: Navigation (M.Map SubworldName) Location , _multiWorld :: MultiWorld Int Entity , _terrainAndEntities :: TerrainEntityMaps + , _recognizerAutomatons :: RecognizerAutomatons RecognizableStructureContent Entity , _worldScrollable :: Bool } @@ -81,6 +84,9 @@ multiWorld :: Lens' Landscape (MultiWorld Int Entity) -- | The catalogs of all terrain and entities that the game knows about. terrainAndEntities :: Lens' Landscape TerrainEntityMaps +-- | Recognition engine for predefined structures +recognizerAutomatons :: Lens' Landscape (RecognizerAutomatons RecognizableStructureContent Entity) + -- | Whether the world map is supposed to be scrollable or not. worldScrollable :: Lens' Landscape Bool @@ -92,6 +98,7 @@ initLandscape gsc = { _worldNavigation = Navigation mempty mempty , _multiWorld = mempty , _terrainAndEntities = initEntityTerrain $ gsiScenarioInputs $ initState gsc + , _recognizerAutomatons = RecognizerAutomatons mempty mempty , _worldScrollable = True } @@ -101,6 +108,7 @@ mkLandscape sLandscape worldTuples theSeed = { _worldNavigation = sLandscape ^. scenarioNavigation , _multiWorld = genMultiWorld worldTuples theSeed , _terrainAndEntities = sLandscape ^. scenarioTerrainAndEntities + , _recognizerAutomatons = sLandscape ^. scenarioStructures . staticAutomatons , -- 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. diff --git a/src/swarm-topography/Swarm/Game/Scenario/Topography/Grid.hs b/src/swarm-topography/Swarm/Game/Scenario/Topography/Grid.hs index 92bb80cdc..1ad46dcec 100644 --- a/src/swarm-topography/Swarm/Game/Scenario/Topography/Grid.hs +++ b/src/swarm-topography/Swarm/Game/Scenario/Topography/Grid.hs @@ -4,12 +4,14 @@ module Swarm.Game.Scenario.Topography.Grid ( Grid (..), NonEmptyGrid (..), gridToVec, - mapWithCoordsNE, mapWithCoords, + mapWithCoordsNE, allMembers, + allMembersNE, mapRowsNE, getRows, mkGrid, + getNonEmptyGrid, ) where @@ -44,6 +46,11 @@ mkGrid rows = fromMaybe EmptyGrid $ do rowsNE <- NE.nonEmpty =<< mapM NE.nonEmpty rows return $ Grid $ NonEmptyGrid rowsNE +getNonEmptyGrid :: Grid a -> Maybe (NonEmptyGrid a) +getNonEmptyGrid = \case + EmptyGrid -> Nothing + Grid x -> Just x + getRows :: Grid a -> [[a]] getRows EmptyGrid = [] getRows (Grid (NonEmptyGrid g)) = NE.toList . NE.map NE.toList $ g diff --git a/src/swarm-topography/Swarm/Game/Scenario/Topography/Structure/Named.hs b/src/swarm-topography/Swarm/Game/Scenario/Topography/Structure/Named.hs index 6db7e6835..c7388c760 100644 --- a/src/swarm-topography/Swarm/Game/Scenario/Topography/Structure/Named.hs +++ b/src/swarm-topography/Swarm/Game/Scenario/Topography/Structure/Named.hs @@ -6,7 +6,6 @@ import Data.Set (Set) import Data.Text (Text) import Data.Yaml import GHC.Generics (Generic) -import Swarm.Game.Scenario.Topography.Grid (Grid) import Swarm.Language.Syntax.Direction (AbsoluteDir) newtype StructureName = StructureName Text @@ -27,9 +26,7 @@ data NamedArea a = NamedArea -- ^ will be UI-facing only if this is a recognizable structure , structure :: a } - deriving (Eq, Show, Functor) + deriving (Eq, Show, Functor, Foldable, Traversable) isRecognizable :: NamedArea a -> Bool isRecognizable = not . null . recognize - -type NamedGrid c = NamedArea (Grid c) diff --git a/src/swarm-topography/Swarm/Game/Scenario/Topography/Structure/Recognition.hs b/src/swarm-topography/Swarm/Game/Scenario/Topography/Structure/Recognition.hs index d16d1b337..e935fa671 100644 --- a/src/swarm-topography/Swarm/Game/Scenario/Topography/Structure/Recognition.hs +++ b/src/swarm-topography/Swarm/Game/Scenario/Topography/Structure/Recognition.hs @@ -13,27 +13,15 @@ import Control.Lens import GHC.Generics (Generic) import Swarm.Game.Scenario.Topography.Structure.Recognition.Log import Swarm.Game.Scenario.Topography.Structure.Recognition.Registry -import Swarm.Game.Scenario.Topography.Structure.Recognition.Type --- | State of the structure recognizer that is intended --- to be modifiable. +-- | +-- The type parameters, `b`, and `a`, correspond +-- to 'Cell' and 'Entity', respectively. data RecognitionState b a = RecognitionState { _foundStructures :: FoundRegistry b a -- ^ Records the top-left corner of the found structure , _recognitionLog :: [SearchLog a] } - -makeLenses ''RecognitionState - --- | --- The type parameters, `b`, and `a`, correspond --- to 'Cell' and 'Entity', respectively. -data StructureRecognizer b a = StructureRecognizer - { _automatons :: RecognizerAutomatons b a - -- ^ read-only - , _recognitionState :: RecognitionState b a - -- ^ mutatable - } deriving (Generic) -makeLenses ''StructureRecognizer +makeLenses ''RecognitionState diff --git a/src/swarm-topography/Swarm/Game/Scenario/Topography/Structure/Recognition/Log.hs b/src/swarm-topography/Swarm/Game/Scenario/Topography/Structure/Recognition/Log.hs index e664e5cc9..ab9699ebc 100644 --- a/src/swarm-topography/Swarm/Game/Scenario/Topography/Structure/Recognition/Log.hs +++ b/src/swarm-topography/Swarm/Game/Scenario/Topography/Structure/Recognition/Log.hs @@ -11,7 +11,7 @@ import GHC.Generics (Generic) import Servant.Docs (ToSample) import Servant.Docs qualified as SD import Swarm.Game.Location (Location) -import Swarm.Game.Scenario.Topography.Structure.Named (StructureName) +import Swarm.Game.Scenario.Topography.Structure.Named (StructureName, name) import Swarm.Game.Scenario.Topography.Structure.Recognition.Static ( OrientedStructure, ) @@ -20,7 +20,7 @@ import Swarm.Game.Universe (Cosmic) renderSharedNames :: ConsolidatedRowReferences b a -> NonEmpty StructureName renderSharedNames = - NE.nub . NE.map (getName . originalDefinition . wholeStructure) . referencingRows + NE.nub . NE.map (name . originalItem . entityGrid . wholeStructure) . referencingRows data ParticipatingEntity e = ParticipatingEntity { entity :: e diff --git a/src/swarm-topography/Swarm/Game/Scenario/Topography/Structure/Recognition/Precompute.hs b/src/swarm-topography/Swarm/Game/Scenario/Topography/Structure/Recognition/Precompute.hs index 6964e350d..a9f621091 100644 --- a/src/swarm-topography/Swarm/Game/Scenario/Topography/Structure/Recognition/Precompute.hs +++ b/src/swarm-topography/Swarm/Game/Scenario/Topography/Structure/Recognition/Precompute.hs @@ -37,7 +37,6 @@ module Swarm.Game.Scenario.Topography.Structure.Recognition.Precompute ( -- * Helper functions populateStaticFoundStructures, - getEntityGrid, lookupStaticPlacements, ensureStructureIntact, ) where @@ -49,10 +48,12 @@ import Control.Monad.Trans.Class (lift) import Control.Monad.Trans.Except (except, runExceptT) import Data.Either.Combinators (leftToMaybe) import Data.Hashable (Hashable) +import Data.List.NonEmpty qualified as NE import Data.Map qualified as M import Data.Maybe (catMaybes, mapMaybe) import Data.Set qualified as Set import Data.Tuple (swap) +import Data.Tuple.Extra (dupe) import Swarm.Game.Location (Location, asVector) import Swarm.Game.Scenario.Topography.Area (getNEGridDimensions, rectWidth) import Swarm.Game.Scenario.Topography.Grid @@ -67,6 +68,7 @@ import Swarm.Game.Scenario.Topography.Structure.Recognition.Registry ( populateStaticFoundStructures, ) import Swarm.Game.Scenario.Topography.Structure.Recognition.Static +import Swarm.Game.Scenario.Topography.Structure.Recognition.Symmetry import Swarm.Game.Scenario.Topography.Structure.Recognition.Type import Swarm.Game.Universe (Cosmic (..), offsetBy, planar) import Swarm.Game.World.Coords (coordsToLoc) @@ -79,85 +81,74 @@ import Swarm.Util (histogram) -- a 'Reader'. -- -- 's' is the state variable, 'a' is the return type. -type GenericEntLocator s a = Cosmic Location -> s (Maybe a) - -getEntityGrid :: (Maybe b -> Maybe a) -> NamedGrid (Maybe b) -> [[Maybe a]] -getEntityGrid extractor = getRows . fmap extractor . structure +type GenericEntLocator s a = Cosmic Location -> s (AtomicKeySymbol a) -- | Create Aho-Corasick matchers that will recognize all of the -- provided structure definitions mkAutomatons :: (Ord a, Hashable a) => - (Maybe b -> Maybe a) -> - [SymmetryAnnotatedGrid (Maybe b)] -> - RecognizerAutomatons (Maybe b) a -mkAutomatons extractor xs = - RecognizerAutomatons - infos - (mkEntityLookup rotatedGrids) + (b -> NonEmptyGrid (AtomicKeySymbol a)) -> + [NamedArea b] -> + Either RedundantOrientations (RecognizerAutomatons b a) +mkAutomatons extractor rawGrids = do + onlyNonempties <- mapM checkSymmetry extractedItems + let rotatedGrids = concatMap (extractGrids . grid) onlyNonempties + infos = + M.fromList $ + map ((name . originalItem . grid . annotatedGrid &&& id) . process) onlyNonempties + return $ + RecognizerAutomatons + infos + (mkEntityLookup rotatedGrids) where - rotatedGrids = concatMap (extractGrids extractor . namedGrid) xs + extractedItems = map (uncurry ExtractedArea . fmap (extractor . structure) . dupe) rawGrids process g = StructureInfo g entGrid countsMap where - entGrid = getEntityGrid extractor $ namedGrid g - countsMap = histogram $ concatMap catMaybes entGrid - - infos = - M.fromList $ - map (name . namedGrid &&& process) xs + entGrid = extractedGrid $ grid g + countsMap = histogram . catMaybes . NE.toList $ allMembersNE entGrid --- | Returns 'Nothing' if the grid is empty, since we want --- to exclude them from the recognition engine. extractOrientedGrid :: - (Maybe b -> Maybe a) -> - NamedGrid (Maybe b) -> + ExtractedArea b a -> AbsoluteDir -> - Maybe (StructureWithGrid (Maybe b) a) -extractOrientedGrid extractor x d = - case extractor <$> structure x of - EmptyGrid -> Nothing - Grid neGrid -> - let w = RowWidth . rectWidth . getNEGridDimensions $ neGrid - in Just $ - StructureWithGrid wrapped d w $ - applyOrientationTransformNE (Orientation d False) neGrid + StructureWithGrid b a +extractOrientedGrid (ExtractedArea x neGrid) d = + StructureWithGrid d w $ + ExtractedArea x $ + applyOrientationTransformNE (Orientation d False) neGrid where - wrapped = NamedOriginal (name x) x + w = RowWidth . rectWidth . getNEGridDimensions $ neGrid -- | -- At this point, we have already ensured that orientations -- redundant by rotational symmetry have been excluded -- (i.e. at Scenario validation time). --- --- Excludes empty grids. extractGrids :: - (Maybe b -> Maybe a) -> - NamedGrid (Maybe b) -> - [StructureWithGrid (Maybe b) a] -extractGrids extractor x = - mapMaybe (extractOrientedGrid extractor x) orientations + ExtractedArea b a -> + [StructureWithGrid b a] +extractGrids x = + map (extractOrientedGrid x) orientations where - orientations = Set.toList $ recognize x + orientations = Set.toList $ recognize $ originalItem x -- | The output list of 'FoundStructure' records is not yet -- vetted; the 'ensureStructureIntact' function will subsequently -- filter this list. lookupStaticPlacements :: - (Maybe b -> Maybe a) -> - StaticStructureInfo b -> - [FoundStructure (Maybe b) a] -lookupStaticPlacements extractor (StaticStructureInfo structDefs thePlacements) = + StaticStructureInfo b a -> + [FoundStructure b a] +lookupStaticPlacements (StaticStructureInfo theAutomatons thePlacements) = concatMap f $ M.toList thePlacements where - definitionMap = M.fromList $ map ((name &&& id) . namedGrid) structDefs + definitionMap = theAutomatons ^. originalStructureDefinitions f (subworldName, locatedList) = mapMaybe g locatedList where g (LocatedStructure (OrientedStructure theName d) loc) = do sGrid <- M.lookup theName definitionMap - x <- extractOrientedGrid extractor sGrid d - return $ PositionedStructure (Cosmic subworldName loc) x + return $ + PositionedStructure (Cosmic subworldName loc) $ + extractOrientedGrid (grid $ annotatedGrid sGrid) d -- | Matches definitions against the placements. -- Fails fast (short-circuits) if a non-matching @@ -171,10 +162,10 @@ ensureStructureIntact :: GenericEntLocator s a -> FoundStructure b a -> s (Maybe (StructureIntactnessFailure a)) -ensureStructureIntact registry entLoader (PositionedStructure upperLeft (StructureWithGrid _ _ _ grid)) = do +ensureStructureIntact registry entLoader (PositionedStructure upperLeft (StructureWithGrid _ _ (ExtractedArea _ g))) = do fmap leftToMaybe . runExceptT $ mapM checkLoc allLocPairs where - gridArea = getNEGridDimensions grid + gridArea = getNEGridDimensions g checkLoc (maybeTemplateEntity, loc) = forM_ maybeTemplateEntity $ \x -> do e <- lift $ entLoader loc @@ -196,4 +187,4 @@ ensureStructureIntact registry entLoader (PositionedStructure upperLeft (Structu . StructureIntactnessFailure (loc ^. planar) gridArea f = fmap ((upperLeft `offsetBy`) . asVector . coordsToLoc) . swap - allLocPairs = mapWithCoordsNE (curry f) grid + allLocPairs = mapWithCoordsNE (curry f) g diff --git a/src/swarm-topography/Swarm/Game/Scenario/Topography/Structure/Recognition/Prep.hs b/src/swarm-topography/Swarm/Game/Scenario/Topography/Structure/Recognition/Prep.hs index f008e6a74..433a1fac4 100644 --- a/src/swarm-topography/Swarm/Game/Scenario/Topography/Structure/Recognition/Prep.hs +++ b/src/swarm-topography/Swarm/Game/Scenario/Topography/Structure/Recognition/Prep.hs @@ -34,7 +34,7 @@ allStructureRows = where transformRows g = imap (StructureRow g . fromIntegral) rows where - NonEmptyGrid rows = entityGrid g + NonEmptyGrid rows = extractedGrid $ entityGrid g -- | If this entity is encountered in the world, -- how far left of it and how far right of it do we need to diff --git a/src/swarm-topography/Swarm/Game/Scenario/Topography/Structure/Recognition/Registry.hs b/src/swarm-topography/Swarm/Game/Scenario/Topography/Structure/Recognition/Registry.hs index e4623ee1b..bf66735d6 100644 --- a/src/swarm-topography/Swarm/Game/Scenario/Topography/Structure/Recognition/Registry.hs +++ b/src/swarm-topography/Swarm/Game/Scenario/Topography/Structure/Recognition/Registry.hs @@ -32,7 +32,7 @@ import Data.Maybe (listToMaybe, maybeToList) import Data.Ord (Down (Down)) import Data.Set qualified as Set import Swarm.Game.Location -import Swarm.Game.Scenario.Topography.Structure.Named (StructureName) +import Swarm.Game.Scenario.Topography.Structure.Named (StructureName, name) import Swarm.Game.Scenario.Topography.Structure.Recognition.Type import Swarm.Game.Universe (Cosmic (..)) import Swarm.Util (binTuples, deleteKeys) @@ -70,7 +70,7 @@ removeStructure fs (FoundRegistry byName byLoc) = (deleteKeys allOccupiedCoords byLoc) where allOccupiedCoords = genOccupiedCoords fs - structureName = getName $ originalDefinition $ structureWithGrid fs + structureName = name . originalItem . entityGrid $ structureWithGrid fs upperLeft = upperLeftCorner fs -- NOTE: Observe similarities to @@ -83,12 +83,12 @@ addFound fs@(PositionedStructure loc swg) (FoundRegistry byName byLoc) = (M.insertWith (<>) k (NEM.singleton loc swg) byName) (M.union occupationMap byLoc) where - k = getName $ originalDefinition swg + k = name . originalItem $ entityGrid swg occupationMap = M.fromList $ map (,fs) $ genOccupiedCoords fs -- | Bulk insertion of structures statically placed in the scenario definition. -- --- See the docs for 'Swarm.Game.State.Initialize.mkRecognizer' for more context. +-- See the docs for 'Swarm.Game.State.Initialize.initializeRecognition' for more context. -- -- Note that if any of these pre-placed structures overlap, we can't be sure of -- the author's intent as to which member of the overlap should take precedence, @@ -117,7 +117,7 @@ populateStaticFoundStructures allFound = byName = M.map (NEM.fromList . NE.map (upperLeftCorner &&& structureWithGrid)) $ binTuples $ - map (getName . originalDefinition . structureWithGrid &&& id) resolvedCollisions + map (name . originalItem . entityGrid . structureWithGrid &&& id) resolvedCollisions resolvePreplacementCollisions foundList = nonOverlappingFound <> maybeToList (listToMaybe overlapsByDecreasingPreference) diff --git a/src/swarm-topography/Swarm/Game/Scenario/Topography/Structure/Recognition/Static.hs b/src/swarm-topography/Swarm/Game/Scenario/Topography/Structure/Recognition/Static.hs index cda28800b..ac972a993 100644 --- a/src/swarm-topography/Swarm/Game/Scenario/Topography/Structure/Recognition/Static.hs +++ b/src/swarm-topography/Swarm/Game/Scenario/Topography/Structure/Recognition/Static.hs @@ -1,18 +1,12 @@ -{-# LANGUAGE TemplateHaskell #-} - -- | -- SPDX-License-Identifier: BSD-3-Clause module Swarm.Game.Scenario.Topography.Structure.Recognition.Static where -import Control.Lens (Lens') import Data.Aeson (ToJSON) -import Data.Map (Map) import GHC.Generics (Generic) import Swarm.Game.Location import Swarm.Game.Scenario.Topography.Structure.Named -import Swarm.Game.Universe (SubworldName) import Swarm.Language.Syntax.Direction (AbsoluteDir) -import Swarm.Util.Lens (makeLensesNoSigs) data RotationalSymmetry = -- | Aka 1-fold symmetry @@ -24,10 +18,10 @@ data RotationalSymmetry deriving (Show, Eq) data SymmetryAnnotatedGrid a = SymmetryAnnotatedGrid - { namedGrid :: NamedGrid a - , symmetry :: RotationalSymmetry + { symmetry :: RotationalSymmetry + , grid :: a } - deriving (Show) + deriving (Show, Functor, Foldable, Traversable) data OrientedStructure = OrientedStructure { oName :: StructureName @@ -48,19 +42,3 @@ data LocatedStructure = LocatedStructure instance HasLocation LocatedStructure where modifyLoc f (LocatedStructure x originalLoc) = LocatedStructure x $ f originalLoc - -data StaticStructureInfo b = StaticStructureInfo - { _structureDefs :: [SymmetryAnnotatedGrid (Maybe b)] - , _staticPlacements :: Map SubworldName [LocatedStructure] - } - deriving (Show) - -makeLensesNoSigs ''StaticStructureInfo - --- | Structure templates that may be auto-recognized when constructed --- by a robot -structureDefs :: Lens' (StaticStructureInfo b) [SymmetryAnnotatedGrid (Maybe b)] - --- | A record of the static placements of structures, so that they can be --- added to the "recognized" list upon scenario initialization -staticPlacements :: Lens' (StaticStructureInfo b) (Map SubworldName [LocatedStructure]) diff --git a/src/swarm-topography/Swarm/Game/Scenario/Topography/Structure/Recognition/Symmetry.hs b/src/swarm-topography/Swarm/Game/Scenario/Topography/Structure/Recognition/Symmetry.hs index 6ca77ad69..448e3d532 100644 --- a/src/swarm-topography/Swarm/Game/Scenario/Topography/Structure/Recognition/Symmetry.hs +++ b/src/swarm-topography/Swarm/Game/Scenario/Topography/Structure/Recognition/Symmetry.hs @@ -6,15 +6,36 @@ -- Symmetry analysis for structure recognizer. module Swarm.Game.Scenario.Topography.Structure.Recognition.Symmetry where -import Control.Monad (unless, when) +import Control.Monad (forM_, when) +import Data.List.NonEmpty (NonEmpty) +import Data.List.NonEmpty qualified as NE import Data.Map qualified as M +import Data.Set (Set) import Data.Set qualified as Set import Data.Text qualified as T -import Swarm.Game.Scenario.Topography.Placement (Orientation (..), applyOrientationTransform) -import Swarm.Game.Scenario.Topography.Structure.Named (NamedGrid, recognize, structure) +import Swarm.Game.Scenario.Topography.Placement (Orientation (..), applyOrientationTransformNE) +import Swarm.Game.Scenario.Topography.Structure.Named (recognize) import Swarm.Game.Scenario.Topography.Structure.Recognition.Static (RotationalSymmetry (..), SymmetryAnnotatedGrid (..)) -import Swarm.Language.Syntax.Direction (AbsoluteDir (DSouth, DWest), getCoordinateOrientation) -import Swarm.Util (commaList, failT, histogram, showT) +import Swarm.Game.Scenario.Topography.Structure.Recognition.Type +import Swarm.Language.Syntax.Direction (AbsoluteDir (DSouth, DWest), CoordinateOrientation, getCoordinateOrientation) +import Swarm.Util (commaList, histogram, showT) + +data RedundantOrientations + = TwoFoldRedundancy (NonEmpty CoordinateOrientation) + | FourFoldRedundancy (Set AbsoluteDir) + +renderRedundancy :: RedundantOrientations -> T.Text +renderRedundancy = \case + TwoFoldRedundancy redundantOrientations -> + T.unwords + [ "Redundant" + , commaList $ map showT $ NE.toList redundantOrientations + , "orientations supplied with two-fold symmetry." + ] + FourFoldRedundancy _xs -> + T.unwords + [ "Redundant orientations supplied; with four-fold symmetry, just supply 'north'." + ] -- | Warns if any recognition orientations are redundant -- by rotational symmetry. @@ -28,23 +49,17 @@ import Swarm.Util (commaList, failT, histogram, showT) -- 2-fold symmetry. -- Warn if two opposite orientations were supplied. checkSymmetry :: - (MonadFail m, Eq a) => NamedGrid a -> m (SymmetryAnnotatedGrid a) -checkSymmetry ng = do + Eq a => + ExtractedArea b a -> + Either RedundantOrientations (SymmetryAnnotatedGrid (ExtractedArea b a)) +checkSymmetry x@(ExtractedArea origObject originalRows) = do case symmetryType of FourFold -> - when (Set.size suppliedOrientations > 1) - . failT - . pure - $ T.unwords ["Redundant orientations supplied; with four-fold symmetry, just supply 'north'."] + when (Set.size suppliedOrientations > 1) . Left $ + FourFoldRedundancy suppliedOrientations TwoFold -> - unless (null redundantOrientations) - . failT - . pure - $ T.unwords - [ "Redundant" - , commaList $ map showT redundantOrientations - , "orientations supplied with two-fold symmetry." - ] + forM_ (NE.nonEmpty redundantOrientations) $ + Left . TwoFoldRedundancy where redundantOrientations = map fst @@ -55,15 +70,14 @@ checkSymmetry ng = do $ Set.toList suppliedOrientations _ -> return () - return $ SymmetryAnnotatedGrid ng symmetryType + return $ SymmetryAnnotatedGrid symmetryType x where symmetryType | quarterTurnRows == originalRows = FourFold | halfTurnRows == originalRows = TwoFold | otherwise = NoSymmetry - quarterTurnRows = applyOrientationTransform (Orientation DWest False) originalRows - halfTurnRows = applyOrientationTransform (Orientation DSouth False) originalRows + quarterTurnRows = applyOrientationTransformNE (Orientation DWest False) originalRows + halfTurnRows = applyOrientationTransformNE (Orientation DSouth False) originalRows - suppliedOrientations = recognize ng - originalRows = structure ng + suppliedOrientations = recognize origObject diff --git a/src/swarm-topography/Swarm/Game/Scenario/Topography/Structure/Recognition/Tracking.hs b/src/swarm-topography/Swarm/Game/Scenario/Topography/Structure/Recognition/Tracking.hs index 9ba609af7..56f6a7791 100644 --- a/src/swarm-topography/Swarm/Game/Scenario/Topography/Structure/Recognition/Tracking.hs +++ b/src/swarm-topography/Swarm/Game/Scenario/Topography/Structure/Recognition/Tracking.hs @@ -10,7 +10,7 @@ module Swarm.Game.Scenario.Topography.Structure.Recognition.Tracking ( ) where import Control.Arrow (left, (&&&)) -import Control.Lens ((%~), (&), (.~), (^.)) +import Control.Lens ((%~), (&), (^.)) import Control.Monad (foldM, guard, unless) import Control.Monad.Extra (findM) import Control.Monad.Trans.Class (lift) @@ -33,6 +33,7 @@ import Data.Semigroup (Max (..), Min (..)) import Data.Tuple (swap) import Linear (V2 (..)) import Swarm.Game.Location (Location) +import Swarm.Game.Scenario.Topography.Structure.Named (name) import Swarm.Game.Scenario.Topography.Structure.Recognition import Swarm.Game.Scenario.Topography.Structure.Recognition.Log import Swarm.Game.Scenario.Topography.Structure.Recognition.Precompute (GenericEntLocator, ensureStructureIntact) @@ -54,48 +55,41 @@ entityModified :: GenericEntLocator s a -> CellModification a -> Cosmic Location -> - StructureRecognizer b a -> - s (StructureRecognizer b a) -entityModified entLoader modification cLoc recognizer = do + RecognizerAutomatons b a -> + RecognitionState b a -> + s (RecognitionState b a) +entityModified entLoader modification cLoc autoRecognizer oldRecognitionState = do (val, accumulatedLogs) <- runWriterT $ case modification of - Add newEntity -> doAddition newEntity recognizer + Add newEntity -> doAddition newEntity oldRecognitionState Remove _ -> doRemoval Swap _ newEntity -> doRemoval >>= doAddition newEntity return $ val - & recognitionState . recognitionLog %~ (reverse accumulatedLogs <>) + & recognitionLog %~ (reverse accumulatedLogs <>) where - entLookup = recognizer ^. automatons . automatonsByEntity - - doAddition newEntity r = do - stateRevision <- case HM.lookup newEntity entLookup of - Nothing -> return oldRecognitionState - Just finder -> do - tell . pure . FoundParticipatingEntity $ - ParticipatingEntity - newEntity - (finder ^. inspectionOffsets) - registerRowMatches entLoader cLoc finder oldRecognitionState - - return $ r & recognitionState .~ stateRevision - where - oldRecognitionState = r ^. recognitionState + entLookup = autoRecognizer ^. automatonsByEntity - doRemoval = do + doAddition newEntity = + maybe return logAndRegister $ HM.lookup newEntity entLookup + where + logAndRegister finder s = do + tell . pure . FoundParticipatingEntity $ + ParticipatingEntity + newEntity + (finder ^. inspectionOffsets) + registerRowMatches entLoader cLoc finder s + + doRemoval = -- Entity was removed; may need to remove registered structure. - stateRevision <- case M.lookup cLoc $ foundByLocation structureRegistry of - Nothing -> return oldRecognitionState - Just fs -> do - tell $ pure $ StructureRemoved structureName - return $ - oldRecognitionState - & foundStructures %~ removeStructure fs - where - structureName = getName $ originalDefinition $ structureWithGrid fs - - return $ recognizer & recognitionState .~ stateRevision + f oldRecognitionState where - oldRecognitionState = recognizer ^. recognitionState + f = maybe return logAndRemove $ M.lookup cLoc $ foundByLocation structureRegistry + logAndRemove fs s = do + tell $ pure $ StructureRemoved structureName + return $ s & foundStructures %~ removeStructure fs + where + structureName = name . originalItem . entityGrid $ structureWithGrid fs + structureRegistry = oldRecognitionState ^. foundStructures -- | In case this cell would match a candidate structure, @@ -111,7 +105,7 @@ candidateEntityAt :: GenericEntLocator s a -> FoundRegistry b a -> Cosmic Location -> - s (Maybe a) + s (AtomicKeySymbol a) candidateEntityAt entLoader registry cLoc = runMaybeT $ do guard $ M.notMember cLoc $ foundByLocation registry MaybeT $ entLoader cLoc @@ -124,7 +118,7 @@ getWorldRow :: FoundRegistry b a -> Cosmic Location -> InspectionOffsets -> - s [Maybe a] + s [AtomicKeySymbol a] getWorldRow entLoader registry cLoc (InspectionOffsets (Min offsetLeft) (Max offsetRight)) = do mapM getCandidate horizontalOffsets where diff --git a/src/swarm-topography/Swarm/Game/Scenario/Topography/Structure/Recognition/Type.hs b/src/swarm-topography/Swarm/Game/Scenario/Topography/Structure/Recognition/Type.hs index 63c06f87a..767da13bc 100644 --- a/src/swarm-topography/Swarm/Game/Scenario/Topography/Structure/Recognition/Type.hs +++ b/src/swarm-topography/Swarm/Game/Scenario/Topography/Structure/Recognition/Type.hs @@ -19,7 +19,7 @@ module Swarm.Game.Scenario.Topography.Structure.Recognition.Type where import Control.Arrow ((&&&)) -import Control.Lens (makeLenses) +import Control.Lens (Lens', makeLenses) import Data.Aeson (ToJSON) import Data.Function (on) import Data.HashMap.Strict (HashMap) @@ -35,11 +35,12 @@ import GHC.Generics (Generic) import Swarm.Game.Location (Location, asVector) import Swarm.Game.Scenario.Topography.Area import Swarm.Game.Scenario.Topography.Grid -import Swarm.Game.Scenario.Topography.Structure.Named (NamedGrid, StructureName) +import Swarm.Game.Scenario.Topography.Structure.Named (NamedArea, StructureName, name) import Swarm.Game.Scenario.Topography.Structure.Recognition.Static -import Swarm.Game.Universe (Cosmic, offsetBy) +import Swarm.Game.Universe (Cosmic, SubworldName, offsetBy) import Swarm.Game.World.Coords (coordsToLoc) import Swarm.Language.Syntax.Direction (AbsoluteDir) +import Swarm.Util.Lens (makeLensesNoSigs) import Text.AhoCorasick (StateMachine) -- | A "needle" consisting of a single cell within @@ -154,13 +155,9 @@ data ConsolidatedRowReferences b a = ConsolidatedRowReferences , theRowWidth :: RowWidth } --- | This wrapper facilitates naming the original structure --- (i.e. the "payload" for recognition) --- for the purpose of both UI display and internal uniqueness, --- while remaining agnostic to its internals. -data NamedOriginal b = NamedOriginal - { getName :: StructureName - , orig :: NamedGrid b +data ExtractedArea b a = ExtractedArea + { originalItem :: NamedArea b + , extractedGrid :: NonEmptyGrid (AtomicKeySymbol a) } deriving (Show, Eq) @@ -170,17 +167,16 @@ data NamedOriginal b = NamedOriginal -- The two type parameters, `b` and `a`, correspond -- to 'Cell' and 'Entity', respectively. data StructureWithGrid b a = StructureWithGrid - { originalDefinition :: NamedOriginal b - , rotatedTo :: AbsoluteDir + { rotatedTo :: AbsoluteDir , gridWidth :: RowWidth - , entityGrid :: NonEmptyGrid (AtomicKeySymbol a) + , entityGrid :: ExtractedArea b a } deriving (Eq) -- | Structure definitions with precomputed metadata for consumption by the UI data StructureInfo b a = StructureInfo - { annotatedGrid :: SymmetryAnnotatedGrid b - , entityProcessedGrid :: [SymbolSequence a] + { annotatedGrid :: SymmetryAnnotatedGrid (ExtractedArea b a) + , entityProcessedGrid :: NonEmptyGrid (AtomicKeySymbol a) , entityCounts :: Map a Int } @@ -287,7 +283,7 @@ data EntityDiscrepancy e = EntityDiscrepancy deriving (Functor, Generic, ToJSON) distillLabel :: StructureWithGrid b a -> OrientedStructure -distillLabel swg = OrientedStructure (getName $ originalDefinition swg) (rotatedTo swg) +distillLabel swg = OrientedStructure (name $ originalItem $ entityGrid swg) (rotatedTo swg) data IntactnessFailureReason e = DiscrepantEntity (EntityDiscrepancy e) @@ -316,7 +312,7 @@ data StructureIntactnessFailure e = StructureIntactnessFailure instance (Eq b, Eq a) => Ord (FoundStructure b a) where compare = compare `on` (f1 &&& f2) where - f1 = computeArea . getNEGridDimensions . entityGrid . structureWithGrid + f1 = computeArea . getNEGridDimensions . extractedGrid . entityGrid . structureWithGrid f2 = Down . upperLeftCorner -- | Yields coordinates that are occupied by an entity of a placed structure. @@ -324,7 +320,21 @@ instance (Eq b, Eq a) => Ord (FoundStructure b a) where -- are not included. genOccupiedCoords :: FoundStructure b a -> [Cosmic Location] genOccupiedCoords (PositionedStructure loc swg) = - catMaybes . NE.toList . mapWithCoordsNE f $ entityGrid swg + catMaybes . NE.toList . mapWithCoordsNE f . extractedGrid $ entityGrid swg where -- replaces an "occupied" grid cell with its location f cellLoc maybeEnt = ((loc `offsetBy`) . asVector . coordsToLoc $ cellLoc) <$ maybeEnt + +data StaticStructureInfo b a = StaticStructureInfo + { _staticAutomatons :: RecognizerAutomatons b a + , _staticPlacements :: Map SubworldName [LocatedStructure] + } + +makeLensesNoSigs ''StaticStructureInfo + +-- | Recognition engine for statically-defined structures +staticAutomatons :: Lens' (StaticStructureInfo b a) (RecognizerAutomatons b a) + +-- | A record of the static placements of structures, so that they can be +-- added to the "recognized" list upon scenario initialization +staticPlacements :: Lens' (StaticStructureInfo b a) (Map SubworldName [LocatedStructure]) diff --git a/src/swarm-tui/Swarm/TUI/Controller/EventHandlers/Main.hs b/src/swarm-tui/Swarm/TUI/Controller/EventHandlers/Main.hs index 8f958d443..918c727d5 100644 --- a/src/swarm-tui/Swarm/TUI/Controller/EventHandlers/Main.hs +++ b/src/swarm-tui/Swarm/TUI/Controller/EventHandlers/Main.hs @@ -13,9 +13,9 @@ import Brick.Keybindings import Control.Lens as Lens import Control.Monad (unless, void, when) import Control.Monad.IO.Class (liftIO) -import Swarm.Game.Scenario.Topography.Structure.Recognition (automatons) import Swarm.Game.Scenario.Topography.Structure.Recognition.Type (originalStructureDefinitions) import Swarm.Game.State +import Swarm.Game.State.Landscape import Swarm.Game.State.Substate import Swarm.Game.Step (finishGameTick) import Swarm.TUI.Controller.EventHandlers.Frame (runGameTickUI) @@ -41,7 +41,7 @@ mainEventHandlers = allHandlers Main $ \case ViewRecipesEvent -> ("View Recipes screen", toggleDiscoveryNotificationModal RecipesModal availableRecipes) ViewCommandsEvent -> ("View Commands screen", toggleDiscoveryNotificationModal CommandsModal availableCommands) ViewMessagesEvent -> ("View Messages screen", toggleMessagesModal) - ViewStructuresEvent -> ("View Structures screen", toggleDiscoveryModal StructuresModal (structureRecognition . automatons . originalStructureDefinitions)) + ViewStructuresEvent -> ("View Structures screen", toggleStructuresModal StructuresModal (recognizerAutomatons . originalStructureDefinitions)) ViewGoalEvent -> ("View scenario goal description", viewGoal) HideRobotsEvent -> ("Hide robots for a few ticks", hideRobots) ShowCESKDebugEvent -> ("Show active robot CESK machine debugging line", showCESKDebug) @@ -72,8 +72,8 @@ toggleGameModal m l = do unless nothingToShow $ toggleModal m return nothingToShow -toggleDiscoveryModal :: Foldable t => ModalType -> Lens' Discovery (t a) -> EventM Name AppState () -toggleDiscoveryModal m l = void $ toggleGameModal m (discovery . l) +toggleStructuresModal :: Foldable t => ModalType -> Lens' Landscape (t a) -> EventM Name AppState () +toggleStructuresModal m l = void $ toggleGameModal m (landscape . l) toggleDiscoveryNotificationModal :: ModalType -> Lens' Discovery (Notifications a) -> EventM Name AppState () toggleDiscoveryNotificationModal m l = do diff --git a/src/swarm-tui/Swarm/TUI/Model/Dialog/Structure.hs b/src/swarm-tui/Swarm/TUI/Model/Dialog/Structure.hs index 6635524ee..d185d4b62 100644 --- a/src/swarm-tui/Swarm/TUI/Model/Dialog/Structure.hs +++ b/src/swarm-tui/Swarm/TUI/Model/Dialog/Structure.hs @@ -12,12 +12,12 @@ import Brick.Widgets.List qualified as BL import Control.Lens (makeLenses) import Data.List.Extra (enumerate) import Swarm.Game.Entity (Entity) -import Swarm.Game.Scenario.Topography.Cell (Cell) +import Swarm.Game.Scenario (RecognizableStructureContent) import Swarm.Game.Scenario.Topography.Structure.Recognition.Type import Swarm.TUI.Model.Name data StructureDisplay = StructureDisplay - { _structurePanelListWidget :: BL.List Name (StructureInfo (Maybe Cell) Entity) + { _structurePanelListWidget :: BL.List Name (StructureInfo RecognizableStructureContent Entity) -- ^ required for maintaining the selection/navigation -- state among list items , _structurePanelFocus :: FocusRing Name diff --git a/src/swarm-tui/Swarm/TUI/Model/StateUpdate.hs b/src/swarm-tui/Swarm/TUI/Model/StateUpdate.hs index acba8dc2b..5b5b04179 100644 --- a/src/swarm-tui/Swarm/TUI/Model/StateUpdate.hs +++ b/src/swarm-tui/Swarm/TUI/Model/StateUpdate.hs @@ -59,7 +59,6 @@ import Swarm.Game.Scenario.Scoring.Best import Swarm.Game.Scenario.Scoring.ConcreteMetrics import Swarm.Game.Scenario.Scoring.GenericMetrics import Swarm.Game.Scenario.Status -import Swarm.Game.Scenario.Topography.Structure.Recognition (automatons) import Swarm.Game.Scenario.Topography.Structure.Recognition.Type (originalStructureDefinitions) import Swarm.Game.ScenarioInfo ( loadScenarioInfo, @@ -280,7 +279,7 @@ scenarioToUIState isAutoplaying siPair@(scenario, _) gs u = do & uiGameplay . uiWorldEditor . EM.editingBounds . EM.boundsRect %~ setNewBounds & uiGameplay . uiDialogs . uiStructure .~ StructureDisplay - (SR.makeListWidget . M.elems $ gs ^. discovery . structureRecognition . automatons . originalStructureDefinitions) + (SR.makeListWidget . M.elems $ gs ^. landscape . recognizerAutomatons . originalStructureDefinitions) (focusSetCurrent (StructureWidgets StructuresList) $ focusRing $ map StructureWidgets enumerate) where entityList = EU.getEntitiesForList $ gs ^. landscape . terrainAndEntities . entityMap diff --git a/src/swarm-tui/Swarm/TUI/View.hs b/src/swarm-tui/Swarm/TUI/View.hs index 64f2a179e..0c738ddc0 100644 --- a/src/swarm-tui/Swarm/TUI/View.hs +++ b/src/swarm-tui/Swarm/TUI/View.hs @@ -95,7 +95,6 @@ import Swarm.Game.Scenario.Scoring.ConcreteMetrics import Swarm.Game.Scenario.Scoring.GenericMetrics import Swarm.Game.Scenario.Status import Swarm.Game.Scenario.Topography.Center -import Swarm.Game.Scenario.Topography.Structure.Recognition (automatons) import Swarm.Game.Scenario.Topography.Structure.Recognition.Type import Swarm.Game.ScenarioInfo ( ScenarioItem (..), @@ -830,7 +829,7 @@ drawModalMenu s = vLimit 1 . hBox $ map (padLeftRight 1 . drawKeyCmd) globalKeyC -- Hides this key if the recognizable structure list is empty structuresKey = - if null $ s ^. gameState . discovery . structureRecognition . automatons . originalStructureDefinitions + if null $ s ^. gameState . landscape . recognizerAutomatons . originalStructureDefinitions then Nothing else Just (NoHighlight, keyM SE.ViewStructuresEvent, "Structures") diff --git a/src/swarm-tui/Swarm/TUI/View/CellDisplay.hs b/src/swarm-tui/Swarm/TUI/View/CellDisplay.hs index 7f6a5f01c..d768c1bb3 100644 --- a/src/swarm-tui/Swarm/TUI/View/CellDisplay.hs +++ b/src/swarm-tui/Swarm/TUI/View/CellDisplay.hs @@ -36,7 +36,7 @@ import Swarm.Game.Land import Swarm.Game.Location (Point (..), toHeading) import Swarm.Game.Robot import Swarm.Game.Scenario.Topography.EntityFacade -import Swarm.Game.Scenario.Topography.Structure.Recognition (foundStructures, recognitionState) +import Swarm.Game.Scenario.Topography.Structure.Recognition (foundStructures) import Swarm.Game.Scenario.Topography.Structure.Recognition.Registry (foundByLocation) import Swarm.Game.State import Swarm.Game.State.Landscape @@ -76,7 +76,7 @@ drawLoc ui g cCoords@(Cosmic _ coords) = boldStructure = applyWhen isStructure $ modifyDefAttr (`V.withStyle` V.bold) where - sMap = foundByLocation $ g ^. discovery . structureRecognition . recognitionState . foundStructures + sMap = foundByLocation $ g ^. discovery . structureRecognition . foundStructures isStructure = M.member (coordsToLoc <$> cCoords) sMap -- | Subset of the game state needed to render the world diff --git a/src/swarm-tui/Swarm/TUI/View/Structure.hs b/src/swarm-tui/Swarm/TUI/View/Structure.hs index 36221c241..26484a09c 100644 --- a/src/swarm-tui/Swarm/TUI/View/Structure.hs +++ b/src/swarm-tui/Swarm/TUI/View/Structure.hs @@ -21,10 +21,9 @@ import Data.Text qualified as T import Data.Vector qualified as V import Swarm.Game.Entity (Entity, entityDisplay) import Swarm.Game.Scenario.Topography.Area -import Swarm.Game.Scenario.Topography.Cell (Cell, cellToEntity) +import Swarm.Game.Scenario.Topography.Grid import Swarm.Game.Scenario.Topography.Structure.Named qualified as Structure -import Swarm.Game.Scenario.Topography.Structure.Recognition (foundStructures, recognitionState) -import Swarm.Game.Scenario.Topography.Structure.Recognition.Precompute (getEntityGrid) +import Swarm.Game.Scenario.Topography.Structure.Recognition (foundStructures) import Swarm.Game.Scenario.Topography.Structure.Recognition.Registry (foundByName) import Swarm.Game.Scenario.Topography.Structure.Recognition.Static import Swarm.Game.Scenario.Topography.Structure.Recognition.Type @@ -41,7 +40,7 @@ import Swarm.Util (commaList) -- | Render a two-pane widget with structure selection on the left -- and single-structure details on the right. -structureWidget :: GameState -> StructureInfo (Maybe Cell) Entity -> Widget n +structureWidget :: GameState -> StructureInfo b Entity -> Widget n structureWidget gs s = vBox [ hBox @@ -50,7 +49,7 @@ structureWidget gs s = . headerItem "Size" . T.pack . renderRectDimensions - . getAreaDimensions + . getNEGridDimensions $ entityProcessedGrid s , occurrenceCountSuffix ] @@ -70,8 +69,8 @@ structureWidget gs s = ] annotatedStructureGrid = annotatedGrid s - - supportedOrientations = Set.toList . Structure.recognize . namedGrid $ annotatedStructureGrid + theNamedGrid = originalItem $ grid annotatedStructureGrid + supportedOrientations = Set.toList . Structure.recognize $ theNamedGrid renderSymmetry = \case NoSymmetry -> "no" @@ -90,16 +89,14 @@ structureWidget gs s = maybeDescriptionWidget = maybe emptyWidget (padTop (Pad 1) . withAttr italicAttr . txtWrap) $ - Structure.description . namedGrid . annotatedGrid $ - s + Structure.description theNamedGrid - registry = gs ^. discovery . structureRecognition . recognitionState . foundStructures + registry = gs ^. discovery . structureRecognition . foundStructures occurrenceCountSuffix = case M.lookup theName $ foundByName registry of Nothing -> emptyWidget Just inner -> padLeft (Pad 2) . headerItem "Count" . T.pack . show $ NEM.size inner structureIllustration = vBox $ map (hBox . map renderOneCell) cells - d = namedGrid $ annotatedGrid s ingredientsBox = vBox @@ -118,15 +115,19 @@ structureWidget gs s = ] ] - theName = Structure.name d - cells = getEntityGrid cellToEntity d + theName = Structure.name theNamedGrid + cells = getRows $ Grid $ entityProcessedGrid s + renderOneCell = maybe (txt " ") (renderDisplay . view entityDisplay) -makeListWidget :: [StructureInfo (Maybe Cell) Entity] -> BL.List Name (StructureInfo (Maybe Cell) Entity) +makeListWidget :: [StructureInfo b a] -> BL.List Name (StructureInfo b a) makeListWidget structureDefinitions = BL.listMoveTo 0 $ BL.list (StructureWidgets StructuresList) (V.fromList structureDefinitions) 1 -renderStructuresDisplay :: GameState -> StructureDisplay -> Widget Name +renderStructuresDisplay :: + GameState -> + StructureDisplay -> + Widget Name renderStructuresDisplay gs structureDisplay = vBox [ hBox @@ -163,7 +164,7 @@ renderStructuresDisplay gs structureDisplay = drawSidebarListItem :: Bool -> - StructureInfo (Maybe Cell) Entity -> + StructureInfo b a -> Widget Name drawSidebarListItem _isSelected (StructureInfo annotated _ _) = - txt . Structure.getStructureName . Structure.name $ namedGrid annotated + txt . Structure.getStructureName . Structure.name $ originalItem $ grid annotated diff --git a/src/swarm-util/Swarm/Language/Syntax/Direction.hs b/src/swarm-util/Swarm/Language/Syntax/Direction.hs index daa4e061e..5bc210642 100644 --- a/src/swarm-util/Swarm/Language/Syntax/Direction.hs +++ b/src/swarm-util/Swarm/Language/Syntax/Direction.hs @@ -10,6 +10,7 @@ module Swarm.Language.Syntax.Direction ( -- * Directions Direction (..), + CoordinateOrientation (..), AbsoluteDir (..), RelativeDir (..), PlanarRelativeDir (..), diff --git a/src/swarm-web/Swarm/Web.hs b/src/swarm-web/Swarm/Web.hs index 5760b1716..735cb0cc6 100644 --- a/src/swarm-web/Swarm/Web.hs +++ b/src/swarm-web/Swarm/Web.hs @@ -231,12 +231,12 @@ recogLogHandler appStateRef = do appState <- liftIO appStateRef return $ map (fmap (view entityName)) $ - appState ^. gameState . discovery . structureRecognition . recognitionState . recognitionLog + appState ^. gameState . discovery . structureRecognition . recognitionLog recogFoundHandler :: IO AppState -> Handler [StructureLocation] recogFoundHandler appStateRef = do appState <- liftIO appStateRef - let registry = appState ^. gameState . discovery . structureRecognition . recognitionState . foundStructures + let registry = appState ^. gameState . discovery . structureRecognition . foundStructures return . map (uncurry StructureLocation) . concatMap (\(x, ys) -> map (x,) $ NE.toList ys) diff --git a/swarm.cabal b/swarm.cabal index 10f8242f5..ad8e25b49 100644 --- a/swarm.cabal +++ b/swarm.cabal @@ -685,6 +685,7 @@ library swarm-engine servant-docs, text, time, + hashable, transformers, unordered-containers, witch,