From abc7a123933100b1d551c72a50d11eb159d3ff9f Mon Sep 17 00:00:00 2001 From: Karl Ostmo Date: Sat, 9 Dec 2023 18:18:59 -0800 Subject: [PATCH] recognize structures with rotation --- .../Challenges/Ranching/beekeeping.yaml | 4 +- .../1575-structure-recognizer/00-ORDER.txt | 2 + .../1575-bounding-box-overlap.yaml | 2 +- .../1575-browse-structures.yaml | 10 ++-- .../1575-construction-count.yaml | 2 +- .../1575-ensure-disjoint.yaml | 2 +- .../1575-ensure-single-recognition.yaml | 2 +- .../1575-floorplan-command.yaml | 2 +- .../1575-handle-overlapping.yaml | 4 +- .../1575-interior-entity-placement.yaml | 2 +- .../1575-nested-structure-definition.yaml | 4 +- ...575-overlapping-tiebreaker-by-largest.yaml | 4 +- ...75-overlapping-tiebreaker-by-location.yaml | 4 +- .../1575-placement-occlusion.yaml | 4 +- .../1575-remove-structure.yaml | 2 +- .../1575-swap-structure.yaml | 6 +- ...1644-rotated-preplacement-recognition.yaml | 53 ++++++++++++++++++ .../1644-rotated-recognition.yaml | 51 +++++++++++++++++ ...zed-placements-disallow-reorientation.yaml | 2 +- data/schema/named-structure.json | 2 +- src/Swarm/Game/Scenario.hs | 2 +- .../Game/Scenario/Topography/Structure.hs | 56 ++++++++++++++----- .../Topography/Structure/Recognition/Log.hs | 9 ++- .../Structure/Recognition/Precompute.hs | 28 +++++++--- .../Topography/Structure/Recognition/Type.hs | 6 +- src/Swarm/Game/State.hs | 16 ++++-- src/Swarm/Game/Step/Const.hs | 2 +- src/Swarm/Language/Direction.hs | 1 + src/Swarm/TUI/Controller.hs | 4 +- src/Swarm/TUI/Model/StateUpdate.hs | 4 +- src/Swarm/TUI/View.hs | 2 +- src/Swarm/TUI/View/Structure.hs | 27 +++++++-- test/integration/Main.hs | 2 + 33 files changed, 254 insertions(+), 69 deletions(-) create mode 100644 data/scenarios/Testing/1575-structure-recognizer/1644-rotated-preplacement-recognition.yaml create mode 100644 data/scenarios/Testing/1575-structure-recognizer/1644-rotated-recognition.yaml diff --git a/data/scenarios/Challenges/Ranching/beekeeping.yaml b/data/scenarios/Challenges/Ranching/beekeeping.yaml index 75d251d9b4..6fc32dceb4 100644 --- a/data/scenarios/Challenges/Ranching/beekeeping.yaml +++ b/data/scenarios/Challenges/Ranching/beekeeping.yaml @@ -125,7 +125,7 @@ solution: | run "scenarios/Challenges/Ranching/_beekeeping/solution.sw" structures: - name: beehive - recognize: true + recognize: [north] structure: palette: '-': [dirt, honey frame] @@ -137,7 +137,7 @@ structures: b---b bbbbb - name: mead hall - recognize: true + recognize: [north] structure: palette: 'w': [dirt, wall] diff --git a/data/scenarios/Testing/1575-structure-recognizer/00-ORDER.txt b/data/scenarios/Testing/1575-structure-recognizer/00-ORDER.txt index 312239f7cc..b6b940335d 100644 --- a/data/scenarios/Testing/1575-structure-recognizer/00-ORDER.txt +++ b/data/scenarios/Testing/1575-structure-recognizer/00-ORDER.txt @@ -12,3 +12,5 @@ 1575-interior-entity-placement.yaml 1575-floorplan-command.yaml 1575-bounding-box-overlap.yaml +1644-rotated-recognition.yaml +1644-rotated-preplacement-recognition.yaml diff --git a/data/scenarios/Testing/1575-structure-recognizer/1575-bounding-box-overlap.yaml b/data/scenarios/Testing/1575-structure-recognizer/1575-bounding-box-overlap.yaml index d14dcd50f9..65578906e0 100644 --- a/data/scenarios/Testing/1575-structure-recognizer/1575-bounding-box-overlap.yaml +++ b/data/scenarios/Testing/1575-structure-recognizer/1575-bounding-box-overlap.yaml @@ -53,7 +53,7 @@ solution: | doN 3 (place "boulder"; move;); structures: - name: chevron - recognize: true + recognize: [north] structure: palette: 'g': [stone, boulder] diff --git a/data/scenarios/Testing/1575-structure-recognizer/1575-browse-structures.yaml b/data/scenarios/Testing/1575-structure-recognizer/1575-browse-structures.yaml index 984668f32d..8500c59f4f 100644 --- a/data/scenarios/Testing/1575-structure-recognizer/1575-browse-structures.yaml +++ b/data/scenarios/Testing/1575-structure-recognizer/1575-browse-structures.yaml @@ -4,7 +4,7 @@ description: | Hit *F6* to view the recognizable structures. Only the subset of the structures marked with - *recognize: true* are browseable. + *recognize: [north]* are browseable. In particular, the `donut`{=structure} structure is placed in the map but not displayed in the *F6* dialog. creative: false @@ -53,7 +53,7 @@ structures: @@@@@ .@@@. - name: diamond - recognize: true + recognize: [north] description: "A diamond pattern of flowers" structure: mask: '.' @@ -68,7 +68,7 @@ structures: ..xxx.. ...x... - name: contraption - recognize: true + recognize: [north] description: "A device for assembling useful widgets" structure: mask: '.' @@ -83,7 +83,7 @@ structures: lIIIgg rlllgg - name: precious - recognize: true + recognize: [north] structure: mask: '.' palette: @@ -96,7 +96,7 @@ structures: gsq qqm - name: smallish - recognize: true + recognize: [north] structure: mask: '.' palette: diff --git a/data/scenarios/Testing/1575-structure-recognizer/1575-construction-count.yaml b/data/scenarios/Testing/1575-structure-recognizer/1575-construction-count.yaml index b6a0ad3b91..2bada02ad7 100644 --- a/data/scenarios/Testing/1575-structure-recognizer/1575-construction-count.yaml +++ b/data/scenarios/Testing/1575-structure-recognizer/1575-construction-count.yaml @@ -37,7 +37,7 @@ solution: | ); structures: - name: green_jewel - recognize: true + recognize: [north] structure: palette: 'g': [stone, pixel (G)] diff --git a/data/scenarios/Testing/1575-structure-recognizer/1575-ensure-disjoint.yaml b/data/scenarios/Testing/1575-structure-recognizer/1575-ensure-disjoint.yaml index 15dbbf7af2..f62d2c4b55 100644 --- a/data/scenarios/Testing/1575-structure-recognizer/1575-ensure-disjoint.yaml +++ b/data/scenarios/Testing/1575-structure-recognizer/1575-ensure-disjoint.yaml @@ -54,7 +54,7 @@ solution: | place "silver"; structures: - name: chessboard - recognize: true + recognize: [north] structure: mask: '.' palette: diff --git a/data/scenarios/Testing/1575-structure-recognizer/1575-ensure-single-recognition.yaml b/data/scenarios/Testing/1575-structure-recognizer/1575-ensure-single-recognition.yaml index 539162170c..e3fb193e85 100644 --- a/data/scenarios/Testing/1575-structure-recognizer/1575-ensure-single-recognition.yaml +++ b/data/scenarios/Testing/1575-structure-recognizer/1575-ensure-single-recognition.yaml @@ -51,7 +51,7 @@ solution: | place "gold"; structures: - name: chessboard - recognize: true + recognize: [north] structure: mask: '.' palette: diff --git a/data/scenarios/Testing/1575-structure-recognizer/1575-floorplan-command.yaml b/data/scenarios/Testing/1575-structure-recognizer/1575-floorplan-command.yaml index aa45705abe..bea495c0b3 100644 --- a/data/scenarios/Testing/1575-structure-recognizer/1575-floorplan-command.yaml +++ b/data/scenarios/Testing/1575-structure-recognizer/1575-floorplan-command.yaml @@ -47,7 +47,7 @@ solution: | mkRows height width; structures: - name: wooden box - recognize: true + recognize: [north] structure: palette: 'b': [stone, board] diff --git a/data/scenarios/Testing/1575-structure-recognizer/1575-handle-overlapping.yaml b/data/scenarios/Testing/1575-structure-recognizer/1575-handle-overlapping.yaml index d5cfe24291..593da87313 100644 --- a/data/scenarios/Testing/1575-structure-recognizer/1575-handle-overlapping.yaml +++ b/data/scenarios/Testing/1575-structure-recognizer/1575-handle-overlapping.yaml @@ -33,7 +33,7 @@ solution: | place "mithril"; structures: - name: precious - recognize: true + recognize: [north] structure: mask: '.' palette: @@ -46,7 +46,7 @@ structures: gsq qqm - name: smallish - recognize: true + recognize: [north] structure: mask: '.' palette: diff --git a/data/scenarios/Testing/1575-structure-recognizer/1575-interior-entity-placement.yaml b/data/scenarios/Testing/1575-structure-recognizer/1575-interior-entity-placement.yaml index 2ce19e1fb1..1bac68c6c3 100644 --- a/data/scenarios/Testing/1575-structure-recognizer/1575-interior-entity-placement.yaml +++ b/data/scenarios/Testing/1575-structure-recognizer/1575-interior-entity-placement.yaml @@ -76,7 +76,7 @@ solution: | place x; structures: - name: pigpen - recognize: true + recognize: [north] structure: palette: 'b': [stone, board] diff --git a/data/scenarios/Testing/1575-structure-recognizer/1575-nested-structure-definition.yaml b/data/scenarios/Testing/1575-structure-recognizer/1575-nested-structure-definition.yaml index 38f848dbe4..1461435d5f 100644 --- a/data/scenarios/Testing/1575-structure-recognizer/1575-nested-structure-definition.yaml +++ b/data/scenarios/Testing/1575-structure-recognizer/1575-nested-structure-definition.yaml @@ -46,7 +46,7 @@ robots: - treads structures: - name: double ring - recognize: true + recognize: [north] structure: palette: 's': [ice, tree] @@ -68,7 +68,7 @@ structures: .s. ... - name: flowerbox - recognize: true + recognize: [north] structure: palette: 'f': [ice, flower] diff --git a/data/scenarios/Testing/1575-structure-recognizer/1575-overlapping-tiebreaker-by-largest.yaml b/data/scenarios/Testing/1575-structure-recognizer/1575-overlapping-tiebreaker-by-largest.yaml index 399c5cd23b..bbc943ad89 100644 --- a/data/scenarios/Testing/1575-structure-recognizer/1575-overlapping-tiebreaker-by-largest.yaml +++ b/data/scenarios/Testing/1575-structure-recognizer/1575-overlapping-tiebreaker-by-largest.yaml @@ -36,7 +36,7 @@ solution: | place "gold"; structures: - name: large - recognize: true + recognize: [north] structure: mask: '.' palette: @@ -47,7 +47,7 @@ structures: ggs ggs - name: small - recognize: true + recognize: [north] structure: mask: '.' palette: diff --git a/data/scenarios/Testing/1575-structure-recognizer/1575-overlapping-tiebreaker-by-location.yaml b/data/scenarios/Testing/1575-structure-recognizer/1575-overlapping-tiebreaker-by-location.yaml index feaec0142d..f9cbbc2ce7 100644 --- a/data/scenarios/Testing/1575-structure-recognizer/1575-overlapping-tiebreaker-by-location.yaml +++ b/data/scenarios/Testing/1575-structure-recognizer/1575-overlapping-tiebreaker-by-location.yaml @@ -37,7 +37,7 @@ solution: | place "gold"; structures: - name: topleft - recognize: true + recognize: [north] structure: mask: '.' palette: @@ -48,7 +48,7 @@ structures: gg gg - name: bottomright - recognize: true + recognize: [north] structure: mask: '.' palette: diff --git a/data/scenarios/Testing/1575-structure-recognizer/1575-placement-occlusion.yaml b/data/scenarios/Testing/1575-structure-recognizer/1575-placement-occlusion.yaml index b9a1a0aeed..c3e00418ac 100644 --- a/data/scenarios/Testing/1575-structure-recognizer/1575-placement-occlusion.yaml +++ b/data/scenarios/Testing/1575-structure-recognizer/1575-placement-occlusion.yaml @@ -40,7 +40,7 @@ solution: | noop; structures: - name: red_jewel - recognize: true + recognize: [north] structure: palette: 'r': [stone, pixel (R)] @@ -49,7 +49,7 @@ structures: rrr rrr - name: green_jewel - recognize: true + recognize: [north] structure: palette: 'g': [stone, pixel (G)] diff --git a/data/scenarios/Testing/1575-structure-recognizer/1575-remove-structure.yaml b/data/scenarios/Testing/1575-structure-recognizer/1575-remove-structure.yaml index c0f48b6d6f..745c571f84 100644 --- a/data/scenarios/Testing/1575-structure-recognizer/1575-remove-structure.yaml +++ b/data/scenarios/Testing/1575-structure-recognizer/1575-remove-structure.yaml @@ -36,7 +36,7 @@ solution: | grab; structures: - name: chessboard - recognize: true + recognize: [north] structure: mask: '.' palette: diff --git a/data/scenarios/Testing/1575-structure-recognizer/1575-swap-structure.yaml b/data/scenarios/Testing/1575-structure-recognizer/1575-swap-structure.yaml index 51e1ee94f0..d770e944db 100644 --- a/data/scenarios/Testing/1575-structure-recognizer/1575-swap-structure.yaml +++ b/data/scenarios/Testing/1575-structure-recognizer/1575-swap-structure.yaml @@ -54,7 +54,7 @@ solution: | swap "pixel (B)"; structures: - name: red_jewel - recognize: true + recognize: [north] structure: mask: '.' palette: @@ -68,7 +68,7 @@ structures: gsssg ggggg - name: green_jewel - recognize: true + recognize: [north] structure: mask: '.' palette: @@ -82,7 +82,7 @@ structures: gsssg ggggg - name: blue_jewel - recognize: true + recognize: [north] structure: mask: '.' palette: diff --git a/data/scenarios/Testing/1575-structure-recognizer/1644-rotated-preplacement-recognition.yaml b/data/scenarios/Testing/1575-structure-recognizer/1644-rotated-preplacement-recognition.yaml new file mode 100644 index 0000000000..7eb6a382c0 --- /dev/null +++ b/data/scenarios/Testing/1575-structure-recognizer/1644-rotated-preplacement-recognition.yaml @@ -0,0 +1,53 @@ +version: 1 +name: Rotated pre-placed structure recognition +description: | + Pre-placed structure recognition with rotation +creative: false +objectives: + - teaser: Have structure + goal: + - | + Have a `tee`{=structure} structure + condition: | + foundStructure <- structure "tee" 0; + return $ case foundStructure (\_. false) (\_. true); +robots: + - name: base + dir: [1, 0] + devices: + - grabber + - treads + inventory: + - [4, flower] +solution: | + noop; +structures: + - name: tee + recognize: [north, south, east, west] + description: "A tee pattern of flowers" + structure: + mask: '.' + palette: + 'x': [stone, flower] + map: | + .x. + xxx +known: [flower] +world: + name: root + dsl: | + {blank} + palette: + '.': [grass] + 'x': [stone, flower] + 'B': [grass, null, base] + upperleft: [0, 0] + placements: + - src: tee + offset: [2, 0] + orient: + up: east + map: | + B.... + ..... + ..... diff --git a/data/scenarios/Testing/1575-structure-recognizer/1644-rotated-recognition.yaml b/data/scenarios/Testing/1575-structure-recognizer/1644-rotated-recognition.yaml new file mode 100644 index 0000000000..68c1710f88 --- /dev/null +++ b/data/scenarios/Testing/1575-structure-recognizer/1644-rotated-recognition.yaml @@ -0,0 +1,51 @@ +version: 1 +name: Rotated structure recognition +description: | + Structure recognition with rotation +creative: false +objectives: + - teaser: Build structure + goal: + - | + Build a `tee`{=structure} structure + condition: | + foundStructure <- structure "tee" 0; + return $ case foundStructure (\_. false) (\_. true); +robots: + - name: base + dir: [1, 0] + devices: + - grabber + - treads + inventory: + - [4, flower] +solution: | + move; move; + turn right; + move; move; + place "flower"; +structures: + - name: tee + recognize: [north, south, east, west] + description: "A tee pattern of flowers" + structure: + mask: '.' + palette: + 'x': [stone, flower] + map: | + .x. + xxx +known: [flower] +world: + name: root + dsl: | + {blank} + palette: + '.': [grass] + 'x': [stone, flower] + 'B': [grass, null, base] + upperleft: [0, 0] + map: | + B.x.. + ..xx. + ..... diff --git a/data/scenarios/Testing/_Validation/1575-recognized-placements-disallow-reorientation.yaml b/data/scenarios/Testing/_Validation/1575-recognized-placements-disallow-reorientation.yaml index e1b7551a4c..df6d83f7c7 100644 --- a/data/scenarios/Testing/_Validation/1575-recognized-placements-disallow-reorientation.yaml +++ b/data/scenarios/Testing/_Validation/1575-recognized-placements-disallow-reorientation.yaml @@ -10,7 +10,7 @@ robots: - treads structures: - name: red_jewel - recognize: true + recognize: [east] structure: mask: '.' palette: diff --git a/data/schema/named-structure.json b/data/schema/named-structure.json index 39bc281876..ae244139f3 100644 --- a/data/schema/named-structure.json +++ b/data/schema/named-structure.json @@ -15,7 +15,7 @@ "description": "Description of this substructure" }, "recognize": { - "type": "boolean", + "type": "array", "description": "Whether this structure participates in automatic recognition when constructed" }, "structure": { diff --git a/src/Swarm/Game/Scenario.hs b/src/Swarm/Game/Scenario.hs index a7861684a9..be635dbfd4 100644 --- a/src/Swarm/Game/Scenario.hs +++ b/src/Swarm/Game/Scenario.hs @@ -227,7 +227,7 @@ instance FromJSONE (EntityMap, WorldMap) Scenario where let mergedNavigation = Navigation mergedWaypoints mergedPortals structureInfo = - StaticStructureInfo (filter Structure.recognize namedGrids) + StaticStructureInfo (filter Structure.isRecognizable namedGrids) . M.fromList . NE.toList $ NE.map (worldName &&& placedStructures) allWorlds diff --git a/src/Swarm/Game/Scenario/Topography/Structure.hs b/src/Swarm/Game/Scenario/Topography/Structure.hs index d4ec1f791a..aeef440ffd 100644 --- a/src/Swarm/Game/Scenario/Topography/Structure.hs +++ b/src/Swarm/Game/Scenario/Topography/Structure.hs @@ -17,6 +17,8 @@ import Data.Either.Extra (maybeToEither) import Data.Foldable (foldrM) import Data.Map qualified as M import Data.Maybe (catMaybes) +import Data.Set (Set) +import Data.Set qualified as Set import Data.Text (Text) import Data.Text qualified as T import Data.Yaml as Y @@ -28,7 +30,8 @@ import Swarm.Game.Scenario.Topography.Cell import Swarm.Game.Scenario.Topography.Navigation.Waypoint import Swarm.Game.Scenario.Topography.Placement import Swarm.Game.Scenario.Topography.WorldPalette -import Swarm.Util (failT, quote, showT) +import Swarm.Language.Direction (AbsoluteDir, directionJsonModifier) +import Swarm.Util (commaList, failT, quote, showT) import Swarm.Util.Yaml import Witch (into) @@ -39,14 +42,21 @@ newtype Grid c = Grid data NamedArea a = NamedArea { name :: StructureName - , recognize :: Bool + , recognize :: Set AbsoluteDir -- ^ whether this structure should be registered for automatic recognition + -- and which orientations shall be recognized. + -- The supplied direction indicates which cardinal direction the + -- original map's "North" has been re-oriented to. + -- E.g., 'DWest' represents a rotation of 90 degrees counter-clockwise. , description :: Maybe Text -- ^ will be UI-facing only if this is a recognizable structure , structure :: a } deriving (Eq, Show, Functor) +isRecognizable :: NamedArea a -> Bool +isRecognizable = not . null . recognize + type NamedGrid c = NamedArea (Grid c) type NamedStructure c = NamedArea (PStructure c) @@ -57,7 +67,7 @@ instance FromJSONE (EntityMap, RobotMap) (NamedArea (PStructure (Maybe Cell))) w parseJSONE = withObjectE "named structure" $ \v -> do NamedArea <$> liftE (v .: "name") - <*> liftE (v .:? "recognize" .!= False) + <*> liftE (v .:? "recognize" .!= mempty) <*> liftE (v .:? "description") <*> v ..: "structure" @@ -78,13 +88,14 @@ data Placed c = Placed Placement (NamedStructure c) -- | For use in registering recognizable pre-placed structures data LocatedStructure = LocatedStructure { placedName :: StructureName + , upDirection :: AbsoluteDir , cornerLoc :: Location } deriving (Show) instance HasLocation LocatedStructure where - modifyLoc f (LocatedStructure x originalLoc) = - LocatedStructure x $ f originalLoc + modifyLoc f (LocatedStructure x y originalLoc) = + LocatedStructure x y $ f originalLoc data MergedStructure c = MergedStructure [[c]] [LocatedStructure] [Originated Waypoint] @@ -159,8 +170,8 @@ mergeStructures :: Either Text (MergedStructure (Maybe a)) mergeStructures inheritedStrucDefs parentPlacement (Structure origArea subStructures subPlacements subWaypoints) = do overlays <- elaboratePlacement parentPlacement $ mapM g subPlacements - let wrapPlacement (Placed z ns) = LocatedStructure (name ns) $ offset z - wrappedOverlays = map wrapPlacement $ filter (\(Placed _ ns) -> recognize ns) overlays + let wrapPlacement (Placed z ns) = LocatedStructure (name ns) (up $ orient z) $ offset z + wrappedOverlays = map wrapPlacement $ filter (\(Placed _ ns) -> isRecognizable ns) overlays foldrM (overlaySingleStructure structureMap) (MergedStructure origArea wrappedOverlays originatedWaypoints) @@ -176,14 +187,31 @@ mergeStructures inheritedStrucDefs parentPlacement (Structure origArea subStruct maybeToEither (T.unwords ["Could not look up structure", quote n]) $ sequenceA (placement, M.lookup sName structureMap) - when (recognize ns && orientation /= defaultOrientation) $ - Left $ - T.unwords - [ "Recognizable structure" - , quote n - , "must use default orientation." - ] + let placementDirection = up orientation + recognizedOrientations = recognize ns + when (isRecognizable ns) $ do + when (flipped orientation) $ + Left $ + T.unwords + [ "Placing recognizable structure" + , quote n + , "with flipped orientation is not supported." + ] + when (Set.notMember placementDirection recognizedOrientations) $ + Left $ + T.unwords + [ "Placing recognizable structure" + , quote n + , "with" + , renderDir placementDirection + , "orientation is not supported." + , "Try" + , commaList $ map renderDir $ Set.toList recognizedOrientations + , "instead." + ] return $ uncurry Placed t + where + renderDir = quote . T.pack . directionJsonModifier . show instance FromJSONE (EntityMap, RobotMap) (PStructure (Maybe Cell)) where parseJSONE = withObjectE "structure definition" $ \v -> do diff --git a/src/Swarm/Game/Scenario/Topography/Structure/Recognition/Log.hs b/src/Swarm/Game/Scenario/Topography/Structure/Recognition/Log.hs index 2d545db185..c7b8d00cd9 100644 --- a/src/Swarm/Game/Scenario/Topography/Structure/Recognition/Log.hs +++ b/src/Swarm/Game/Scenario/Topography/Structure/Recognition/Log.hs @@ -46,12 +46,19 @@ data ParticipatingEntity = ParticipatingEntity } deriving (Generic, ToJSON) +data IntactPlacementLog = IntactPlacementLog + { isIntact :: Bool + , sName :: StructureName + , locUpperLeft :: Cosmic Location + } + deriving (Generic, ToJSON) + data SearchLog = FoundParticipatingEntity ParticipatingEntity | StructureRemoved StructureName | FoundRowCandidates [FoundRowCandidate] | FoundCompleteStructureCandidates [StructureName] - | IntactStaticPlacement [(Bool, StructureName, Cosmic Location)] + | IntactStaticPlacement [IntactPlacementLog] deriving (Generic) instance ToJSON SearchLog where diff --git a/src/Swarm/Game/Scenario/Topography/Structure/Recognition/Precompute.hs b/src/Swarm/Game/Scenario/Topography/Structure/Recognition/Precompute.hs index 19098132d8..32fb612124 100644 --- a/src/Swarm/Game/Scenario/Topography/Structure/Recognition/Precompute.hs +++ b/src/Swarm/Game/Scenario/Topography/Structure/Recognition/Precompute.hs @@ -37,7 +37,7 @@ module Swarm.Game.Scenario.Topography.Structure.Recognition.Precompute ( -- * Helper functions populateStaticFoundStructures, getEntityGrid, - extractGrid, + extractGrids, lookupStaticPlacements, ) where @@ -49,14 +49,17 @@ import Data.Map qualified as M import Data.Maybe (catMaybes, mapMaybe) import Data.Semigroup (sconcat) import Data.Set qualified as S +import Data.Set qualified as Set import Data.Tuple (swap) import Swarm.Game.Entity (Entity, entityName) import Swarm.Game.Scenario (StaticStructureInfo (..)) import Swarm.Game.Scenario.Topography.Cell +import Swarm.Game.Scenario.Topography.Placement (Orientation (..), applyOrientationTransform) import Swarm.Game.Scenario.Topography.Structure import Swarm.Game.Scenario.Topography.Structure.Recognition.Registry import Swarm.Game.Scenario.Topography.Structure.Recognition.Type import Swarm.Game.Universe (Cosmic (..)) +import Swarm.Language.Direction (AbsoluteDir) import Swarm.Util (binTuples, histogram) import Swarm.Util.Erasable (erasableToMaybe) import Text.AhoCorasick @@ -96,7 +99,7 @@ mkRowLookup neList = concatMap (concatMap catMaybes . fst) tuples deriveRowOffsets :: StructureRow -> InspectionOffsets - deriveRowOffsets (StructureRow (StructureWithGrid _ g) rwIdx _) = + deriveRowOffsets (StructureRow (StructureWithGrid _ _ g) rwIdx _) = mkOffsets rwIdx g bounds = sconcat $ NE.map deriveRowOffsets neList @@ -167,20 +170,31 @@ mkEntityLookup grids = catMaybes $ zipWith (\idx -> fmap (PositionWithinRow idx r,)) [0 ..] rowMembers +-- | Create Aho-Corasick matchers that will recognize all of the +-- provided structure definitions mkAutomatons :: [NamedGrid (Maybe Cell)] -> RecognizerAutomatons mkAutomatons xs = RecognizerAutomatons infos (mkEntityLookup grids) where - grids = map extractGrid xs + grids = concatMap extractGrids xs process g = StructureInfo g . histogram . concatMap catMaybes $ entityGrid g infos = M.fromList $ map (name . originalDefinition &&& process) grids -extractGrid :: NamedGrid (Maybe Cell) -> StructureWithGrid -extractGrid x = StructureWithGrid x $ getEntityGrid $ structure x +extractOrientedGrid :: NamedGrid (Maybe Cell) -> AbsoluteDir -> StructureWithGrid +extractOrientedGrid x d = StructureWithGrid x d $ getEntityGrid g' + where + Grid rows = structure x + g' = Grid $ applyOrientationTransform (Orientation d False) rows + +extractGrids :: NamedGrid (Maybe Cell) -> [StructureWithGrid] +extractGrids x = map (extractOrientedGrid x) $ Set.toList $ recognize x +-- | The output list of 'FoundStructure' records is not yet +-- vetted; the 'ensureStructureIntact' function will subsequently +-- filter this list. lookupStaticPlacements :: StaticStructureInfo -> [FoundStructure] lookupStaticPlacements (StaticStructureInfo structDefs thePlacements) = concatMap f $ M.toList thePlacements @@ -189,6 +203,6 @@ lookupStaticPlacements (StaticStructureInfo structDefs thePlacements) = f (subworldName, locatedList) = mapMaybe g locatedList where - g (LocatedStructure theName loc) = do + g (LocatedStructure theName d loc) = do sGrid <- M.lookup theName definitionMap - return $ FoundStructure (extractGrid sGrid) $ Cosmic subworldName loc + return $ FoundStructure (extractOrientedGrid sGrid d) $ Cosmic subworldName loc diff --git a/src/Swarm/Game/Scenario/Topography/Structure/Recognition/Type.hs b/src/Swarm/Game/Scenario/Topography/Structure/Recognition/Type.hs index dff979b621..2598a45b45 100644 --- a/src/Swarm/Game/Scenario/Topography/Structure/Recognition/Type.hs +++ b/src/Swarm/Game/Scenario/Topography/Structure/Recognition/Type.hs @@ -38,6 +38,7 @@ import Swarm.Game.Scenario.Topography.Cell import Swarm.Game.Scenario.Topography.Placement (StructureName) import Swarm.Game.Scenario.Topography.Structure (NamedGrid) import Swarm.Game.Universe (Cosmic, offsetBy) +import Swarm.Language.Syntax (AbsoluteDir) import Text.AhoCorasick (StateMachine) -- | A "needle" consisting of a single cell within @@ -129,6 +130,7 @@ data StructureRow = StructureRow -- with its grid of cells having been extracted for convenience. data StructureWithGrid = StructureWithGrid { originalDefinition :: NamedGrid (Maybe Cell) + , rotatedTo :: AbsoluteDir , entityGrid :: [SymbolSequence] } deriving (Eq) @@ -181,9 +183,9 @@ makeLenses ''AutomatonInfo -- | The complete set of data needed to identify applicable -- structures, based on a just-placed entity. data RecognizerAutomatons = RecognizerAutomatons - { _definitions :: Map StructureName StructureInfo + { _originalStructureDefinitions :: Map StructureName StructureInfo -- ^ all of the structures that shall participate in automatic recognition. - -- This list is used only by the UI. + -- This list is used only by the UI and by the 'Floorplan' command. , _automatonsByEntity :: Map Entity (AutomatonInfo AtomicKeySymbol StructureSearcher) } deriving (Generic) diff --git a/src/Swarm/Game/State.hs b/src/Swarm/Game/State.hs index a8b5620acd..c2962aeeed 100644 --- a/src/Swarm/Game/State.hs +++ b/src/Swarm/Game/State.hs @@ -577,7 +577,7 @@ ensureStructureIntact :: (Has (State GameState) sig m) => FoundStructure -> m Bool -ensureStructureIntact (FoundStructure (StructureWithGrid _ grid) upperLeft) = +ensureStructureIntact (FoundStructure (StructureWithGrid _ _ grid) upperLeft) = allM outer $ zip [0 ..] grid where outer (y, row) = allM (inner y) $ zip [0 ..] row @@ -595,12 +595,18 @@ mkRecognizer :: mkRecognizer structInfo@(StaticStructureInfo structDefs _) = do foundIntact <- mapM (sequenceA . (id &&& ensureStructureIntact)) allPlaced let fs = populateStaticFoundStructures . map fst . filter snd $ foundIntact - foundIntactLog = - IntactStaticPlacement $ - map (\(x, isIntact) -> (isIntact, (Structure.name . originalDefinition . structureWithGrid) x, upperLeftCorner x)) foundIntact - return $ StructureRecognizer (mkAutomatons structDefs) fs [foundIntactLog] + return $ + StructureRecognizer + (mkAutomatons structDefs) + fs + [IntactStaticPlacement $ map mkLogEntry foundIntact] where allPlaced = lookupStaticPlacements structInfo + mkLogEntry (x, isIntact) = + IntactPlacementLog + isIntact + ((Structure.name . originalDefinition . structureWithGrid) x) + (upperLeftCorner x) buildTagMap :: EntityMap -> Map Text (NonEmpty EntityName) buildTagMap em = diff --git a/src/Swarm/Game/Step/Const.hs b/src/Swarm/Game/Step/Const.hs index 641a4bb04d..a5207b706f 100644 --- a/src/Swarm/Game/Step/Const.hs +++ b/src/Swarm/Game/Step/Const.hs @@ -511,7 +511,7 @@ execConst runChildProg c vs s k = do _ -> badConst Floorplan -> case vs of [VText name] -> do - structureTemplates <- use $ discovery . structureRecognition . automatons . definitions + structureTemplates <- use $ discovery . structureRecognition . automatons . originalStructureDefinitions let maybeStructure = M.lookup (StructureName name) structureTemplates structureDef <- maybeStructure diff --git a/src/Swarm/Language/Direction.hs b/src/Swarm/Language/Direction.hs index 46bc231eb6..e00fecac31 100644 --- a/src/Swarm/Language/Direction.hs +++ b/src/Swarm/Language/Direction.hs @@ -16,6 +16,7 @@ module Swarm.Language.Direction ( directionSyntax, isCardinal, allDirs, + directionJsonModifier, ) where import Data.Aeson.Types hiding (Key) diff --git a/src/Swarm/TUI/Controller.hs b/src/Swarm/TUI/Controller.hs index 6636a934e3..1a585fc80f 100644 --- a/src/Swarm/TUI/Controller.hs +++ b/src/Swarm/TUI/Controller.hs @@ -79,7 +79,7 @@ import Swarm.Game.Location import Swarm.Game.ResourceLoading (getSwarmHistoryPath) import Swarm.Game.Robot import Swarm.Game.Scenario.Topography.Structure.Recognition (automatons) -import Swarm.Game.Scenario.Topography.Structure.Recognition.Type (definitions) +import Swarm.Game.Scenario.Topography.Structure.Recognition.Type (originalStructureDefinitions) import Swarm.Game.ScenarioInfo import Swarm.Game.State import Swarm.Game.State.Robot @@ -338,7 +338,7 @@ handleMainEvent ev = do FKey 5 | not (null (s ^. gameState . messageNotifications . notificationsContent)) -> do toggleModal MessagesModal gameState . messageInfo . lastSeenMessageTime .= s ^. gameState . temporal . ticks - FKey 6 | not (null $ s ^. gameState . discovery . structureRecognition . automatons . definitions) -> toggleModal StructuresModal + FKey 6 | not (null $ s ^. gameState . discovery . structureRecognition . automatons . originalStructureDefinitions) -> toggleModal StructuresModal -- show goal ControlChar 'g' -> if hasAnythingToShow $ s ^. uiState . uiGoal . goalsContent diff --git a/src/Swarm/TUI/Model/StateUpdate.hs b/src/Swarm/TUI/Model/StateUpdate.hs index b893848a68..dd08808c27 100644 --- a/src/Swarm/TUI/Model/StateUpdate.hs +++ b/src/Swarm/TUI/Model/StateUpdate.hs @@ -51,7 +51,7 @@ 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 (definitions) +import Swarm.Game.Scenario.Topography.Structure.Recognition.Type (originalStructureDefinitions) import Swarm.Game.ScenarioInfo ( loadScenarioInfo, normalizeScenarioPath, @@ -268,7 +268,7 @@ scenarioToUIState isAutoplaying siPair@(scenario, _) gs u = do & uiWorldEditor . EM.editingBounds . EM.boundsRect %~ setNewBounds & uiStructure .~ StructureDisplay - (SR.makeListWidget . M.elems $ gs ^. discovery . structureRecognition . automatons . definitions) + (SR.makeListWidget . M.elems $ gs ^. discovery . structureRecognition . automatons . originalStructureDefinitions) (focusSetCurrent (StructureWidgets StructuresList) $ focusRing $ map StructureWidgets listEnums) where entityList = EU.getEntitiesForList $ gs ^. landscape . entityMap diff --git a/src/Swarm/TUI/View.hs b/src/Swarm/TUI/View.hs index dfa5d7b801..4a1eba17ff 100644 --- a/src/Swarm/TUI/View.hs +++ b/src/Swarm/TUI/View.hs @@ -931,7 +931,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 . definitions + if null $ s ^. gameState . discovery . structureRecognition . automatons . originalStructureDefinitions then Nothing else Just (NoHighlight, "F6", "Structures") diff --git a/src/Swarm/TUI/View/Structure.hs b/src/Swarm/TUI/View/Structure.hs index dc01c52d9a..7c8e2250cf 100644 --- a/src/Swarm/TUI/View/Structure.hs +++ b/src/Swarm/TUI/View/Structure.hs @@ -9,13 +9,15 @@ module Swarm.TUI.View.Structure ( makeListWidget, ) where -import Brick hiding (Direction, Location) +import Brick hiding (Direction, Location, on) import Brick.Focus import Brick.Widgets.Center import Brick.Widgets.List qualified as BL import Control.Lens hiding (Const, from) +import Data.Function (on) import Data.Map.NonEmpty qualified as NEM import Data.Map.Strict qualified as M +import Data.Set qualified as Set import Data.Text qualified as T import Data.Vector qualified as V import Swarm.Game.Entity (entityDisplay) @@ -28,11 +30,13 @@ import Swarm.Game.Scenario.Topography.Structure.Recognition.Registry (foundByNam import Swarm.Game.Scenario.Topography.Structure.Recognition.Type import Swarm.Game.State import Swarm.Game.State.Substate +import Swarm.Language.Direction (AbsoluteDir (DNorth), directionJsonModifier) import Swarm.TUI.Model.Name import Swarm.TUI.Model.Structure import Swarm.TUI.View.Attribute.Attr import Swarm.TUI.View.CellDisplay import Swarm.TUI.View.Util +import Swarm.Util (commaList, listEnums) -- | Render a two-pane widget with structure selection on the left -- and single-structure details on the right. @@ -50,6 +54,7 @@ structureWidget gs s = $ withGrid s , occurrenceCountSuffix ] + , maybeReorientabilityWidget , maybeDescriptionWidget , padTop (Pad 1) $ hBox @@ -64,7 +69,21 @@ structureWidget gs s = , withAttr boldAttr $ txt content ] - maybeDescriptionWidget = maybe emptyWidget txtWrap $ Structure.description . originalDefinition . withGrid $ s + supportedOrientations = Set.toList . Structure.recognize . originalDefinition . withGrid $ s + maybeReorientabilityWidget + | supportedOrientations == [DNorth] = txt "Fixed orientation." + | ((==) `on` length) supportedOrientations listEnums = txt "Reorientable." + | otherwise = + txt $ + T.unwords + [ "Supports orientations:" + , commaList $ map (T.pack . directionJsonModifier . show) supportedOrientations + ] + + maybeDescriptionWidget = + maybe emptyWidget (padTop (Pad 1) . withAttr italicAttr . txtWrap) $ + Structure.description . originalDefinition . withGrid $ + s registry = gs ^. discovery . structureRecognition . foundStructures occurrenceCountSuffix = case M.lookup sName $ foundByName registry of @@ -97,8 +116,8 @@ structureWidget gs s = renderOneCell = maybe (txt " ") (renderDisplay . view entityDisplay) makeListWidget :: [StructureInfo] -> BL.List Name StructureInfo -makeListWidget structureDefs = - BL.listMoveTo 0 $ BL.list (StructureWidgets StructuresList) (V.fromList structureDefs) 1 +makeListWidget structureDefinitions = + BL.listMoveTo 0 $ BL.list (StructureWidgets StructuresList) (V.fromList structureDefinitions) 1 renderStructuresDisplay :: GameState -> StructureDisplay -> Widget Name renderStructuresDisplay gs structureDisplay = diff --git a/test/integration/Main.hs b/test/integration/Main.hs index 1fb5f0cd7c..cfbb05054a 100644 --- a/test/integration/Main.hs +++ b/test/integration/Main.hs @@ -426,6 +426,8 @@ testScenarioSolutions rs ui = , testSolution Default "Testing/1575-structure-recognizer/1575-interior-entity-placement" , testSolution Default "Testing/1575-structure-recognizer/1575-floorplan-command" , testSolution Default "Testing/1575-structure-recognizer/1575-bounding-box-overlap" + , testSolution Default "Testing/1575-structure-recognizer/1644-rotated-recognition" + , testSolution Default "Testing/1575-structure-recognizer/1644-rotated-preplacement-recognition" ] ] , testSolution' Default "Testing/1430-built-robot-ownership" CheckForBadErrors $ \g -> do