recognize structures with rotation (#1678)

Closes #1644.

The `"recognize"` property in scenario `.yaml` files is changed from a boolean to a list of "up" directions.

The structure recognizer adds a rotated copy of each supported orientation to its automaton.  Rotational symmetry is accounted for to avoid duplicate work in the recognizer.

Also in this PR:
* Add cardinal directions to the JSON schema
* Tetromino packing challenge scenario

## Demos

    scripts/run-tests.sh --test-arguments '--pattern "1644-rotated"'

### Structures dialog

![Screenshot from 2023-12-10 18-47-01](https://github.com/swarm-game/swarm/assets/261693/3904b66e-dd22-455b-8b68-5913021f806a)

### Tetromino packing

    scripts/play.sh -i data/scenarios/Challenges/pack-tetrominoes.yaml --autoplay

![Screenshot from 2023-12-09 23-11-00](https://github.com/swarm-game/swarm/assets/261693/0ad7c0ce-3553-4ad5-a927-82bbfdbe63d8)
This commit is contained in:
Karl Ostmo 2023-12-13 13:21:08 -08:00 committed by GitHub
parent e227230bbb
commit 13ae996306
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 635 additions and 85 deletions

View File

@ -16,6 +16,7 @@ wave.yaml
wolf-goat-cabbage.yaml
blender.yaml
friend.yaml
pack-tetrominoes.yaml
Mazes
Ranching
Sokoban

View File

@ -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]

View File

@ -0,0 +1,67 @@
move;
place "j-tile";
move;
place "j-tile";
turn left;
move;
place "j-tile";
move;
place "j-tile";
turn back;
move; move;
turn left;
move;
place "t-tile";
move;
place "t-tile";
turn left;
move;
place "t-tile";
turn back;
move;
turn left;
move;
place "t-tile";
turn back;
move;
move;
turn right;
move;
place "z-tile";
move;
place "z-tile";
turn right;
move;
place "z-tile";
turn left;
move;
place "z-tile";
turn left;
move;
place "i-tile";
move;
place "i-tile";
move;
place "i-tile";
move;
place "i-tile";
turn left;
move;
place "q-tile";
move;
turn left;
place "q-tile";
move;
turn left;
place "q-tile";
move;
turn left;
place "q-tile";

View File

@ -0,0 +1,147 @@
version: 1
name: Tetrominoes
description: |
Pack tetrominoes into a rectangle
creative: false
attrs:
- name: q-tile
fg: "#ffff00"
- name: z-tile
fg: "#ff0000"
- name: i-tile
fg: "#00ffff"
- name: j-tile
fg: "#0000ff"
- name: t-tile
fg: "#ff00ff"
objectives:
- teaser: Place all
goal:
- |
Place all five tetrominoes.
condition: |
def found = \s.
fs <- structure s 0;
return $ case fs (\_. false) (\_. true);
end;
foundT <- found "tee";
foundJ <- found "jay";
foundI <- found "line";
foundZ <- found "zee";
foundQ <- found "square";
return $ foundT && foundJ && foundI && foundZ && foundQ;
robots:
- name: base
dir: [1, 0]
devices:
- grabber
- treads
inventory:
- [4, z-tile]
- [4, j-tile]
- [4, i-tile]
- [4, q-tile]
- [4, t-tile]
entities:
- name: q-tile
display:
char: 'q'
attr: q-tile
description:
- q-tile
properties: [known, portable]
- name: j-tile
display:
char: 'j'
attr: j-tile
description:
- j-tile
properties: [known, portable]
- name: i-tile
display:
char: 'i'
attr: i-tile
description:
- i-tile
properties: [known, portable]
- name: t-tile
display:
char: 't'
attr: t-tile
description:
- t-tile
properties: [known, portable]
- name: z-tile
display:
char: 'z'
attr: z-tile
description:
- z-tile
properties: [known, portable]
solution: |
run "scenarios/Challenges/_pack-tetrominoes/solution.sw"
structures:
- name: tee
recognize: [north, south, east, west]
description: "Tee tetromino"
structure:
mask: '.'
palette:
'x': [stone, t-tile]
map: |
.x.
xxx
- name: square
recognize: [north]
description: "Square tetromino"
structure:
mask: '.'
palette:
'x': [stone, q-tile]
map: |
xx
xx
- name: line
recognize: [north, east]
description: "Line tetromino"
structure:
mask: '.'
palette:
'x': [stone, i-tile]
map: |
xxxx
- name: jay
recognize: [north, east, south, west]
description: "Jay tetromino"
structure:
mask: '.'
palette:
'x': [stone, j-tile]
map: |
xxx
..x
- name: zee
recognize: [north, east]
description: "Zee tetromino"
structure:
mask: '.'
palette:
'x': [stone, z-tile]
map: |
xx.
.xx
known: [boulder]
world:
name: root
dsl: |
{boulder}
palette:
'.': [grass, erase]
'B': [grass, erase, base]
upperleft: [0, 0]
map: |
......
......
......
B.....

View File

@ -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

View File

@ -53,7 +53,7 @@ solution: |
doN 3 (place "boulder"; move;);
structures:
- name: chevron
recognize: true
recognize: [north]
structure:
palette:
'g': [stone, boulder]

View File

@ -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:

View File

@ -37,7 +37,7 @@ solution: |
);
structures:
- name: green_jewel
recognize: true
recognize: [north]
structure:
palette:
'g': [stone, pixel (G)]

View File

@ -54,7 +54,7 @@ solution: |
place "silver";
structures:
- name: chessboard
recognize: true
recognize: [north]
structure:
mask: '.'
palette:

View File

@ -51,7 +51,7 @@ solution: |
place "gold";
structures:
- name: chessboard
recognize: true
recognize: [north]
structure:
mask: '.'
palette:

View File

@ -47,7 +47,7 @@ solution: |
mkRows height width;
structures:
- name: wooden box
recognize: true
recognize: [north]
structure:
palette:
'b': [stone, board]

View File

@ -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:

View File

@ -76,7 +76,7 @@ solution: |
place x;
structures:
- name: pigpen
recognize: true
recognize: [north]
structure:
palette:
'b': [stone, board]

View File

@ -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]

View File

@ -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:

View File

@ -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:

View File

@ -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)]

View File

@ -36,7 +36,7 @@ solution: |
grab;
structures:
- name: chessboard
recognize: true
recognize: [north]
structure:
mask: '.'
palette:

View File

@ -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:

View File

@ -0,0 +1,47 @@
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]
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]
'B': [grass, null, base]
upperleft: [0, 0]
placements:
- src: tee
offset: [2, 0]
orient:
up: east
map: |
B....
.....
.....

View File

@ -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.
.....

View File

@ -10,7 +10,7 @@ robots:
- treads
structures:
- name: red_jewel
recognize: true
recognize: [east]
structure:
mask: '.'
palette:

View File

@ -0,0 +1,29 @@
version: 1
name: Structure recognizer - redundant symmetries
description: |
Prevent orientations from being supplied
for recognition when they are redundant by
rotational symmetry
creative: false
robots:
- name: base
dir: [1, 0]
structures:
- name: domino
recognize: [east, north, west]
structure:
mask: '.'
palette:
'r': [stone, tree]
map: |
r
r
known: [tree]
world:
name: root
palette:
'.': [grass]
'B': [grass, null, base]
upperleft: [0, 0]
map: |
B

View File

@ -0,0 +1,6 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://raw.githubusercontent.com/swarm-game/swarm/main/data/schema/directions.json",
"title": "Directions",
"enum": ["north", "west", "south", "east"]
}

View File

@ -15,8 +15,11 @@
"description": "Description of this substructure"
},
"recognize": {
"type": "boolean",
"description": "Whether this structure participates in automatic recognition when constructed"
"type": "array",
"items": {
"$ref": "directions.json"
},
"description": "Orientations for which this structure participates in automatic recognition when constructed"
},
"structure": {
"$ref": "structure.json"

View File

@ -7,7 +7,7 @@
"additionalProperties": false,
"properties": {
"up": {
"type": "string"
"$ref": "directions.json"
},
"flip": {
"type": "boolean"

View File

@ -84,6 +84,8 @@ import Swarm.Game.Scenario.Topography.Cell
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.Recognition.Symmetry
import Swarm.Game.Scenario.Topography.Structure.Recognition.Type (SymmetryAnnotatedGrid (..))
import Swarm.Game.Scenario.Topography.WorldDescription
import Swarm.Game.Universe
import Swarm.Game.World.Load (loadWorlds)
@ -100,7 +102,7 @@ import System.Directory (doesFileExist)
import System.FilePath ((<.>), (</>))
data StaticStructureInfo = StaticStructureInfo
{ _structureDefs :: [Structure.NamedGrid (Maybe Cell)]
{ _structureDefs :: [SymmetryAnnotatedGrid (Maybe Cell)]
, _staticPlacements :: M.Map SubworldName [Structure.LocatedStructure]
}
deriving (Show)
@ -109,7 +111,7 @@ makeLensesNoSigs ''StaticStructureInfo
-- | Structure templates that may be auto-recognized when constructed
-- by a robot
structureDefs :: Lens' StaticStructureInfo [Structure.NamedGrid (Maybe Cell)]
structureDefs :: Lens' StaticStructureInfo [SymmetryAnnotatedGrid (Maybe Cell)]
-- | A record of the static placements of structures, so that they can be
-- added to the "recognized" list upon scenario initialization
@ -226,8 +228,12 @@ instance FromJSONE (EntityMap, WorldMap) Scenario where
$ NE.toList allWorlds
let mergedNavigation = Navigation mergedWaypoints mergedPortals
structureInfo =
StaticStructureInfo (filter Structure.recognize namedGrids)
recognizableGrids = filter Structure.isRecognizable namedGrids
symmetryAnnotatedGrids <- mapM checkSymmetry recognizableGrids
let structureInfo =
StaticStructureInfo symmetryAnnotatedGrids
. M.fromList
. NE.toList
$ NE.map (worldName &&& placedStructures) allWorlds

View File

@ -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,34 @@ 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."
]
-- Redundant orientations by rotational symmetry are accounted
-- for at scenario parse time
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

View File

@ -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

View File

@ -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,28 +170,42 @@ mkEntityLookup grids =
catMaybes $
zipWith (\idx -> fmap (PositionWithinRow idx r,)) [0 ..] rowMembers
mkAutomatons :: [NamedGrid (Maybe Cell)] -> RecognizerAutomatons
-- | Create Aho-Corasick matchers that will recognize all of the
-- provided structure definitions
mkAutomatons :: [SymmetryAnnotatedGrid (Maybe Cell)] -> RecognizerAutomatons
mkAutomatons xs =
RecognizerAutomatons
infos
(mkEntityLookup grids)
(mkEntityLookup rotatedGrids)
where
grids = map extractGrid xs
rotatedGrids = concatMap (extractGrids . namedGrid) xs
process g = StructureInfo g . histogram . concatMap catMaybes $ entityGrid g
infos = M.fromList $ map (name . originalDefinition &&& process) grids
process g = StructureInfo g (getEntityGrid $ structure $ namedGrid g) . histogram . concatMap catMaybes . getEntityGrid . structure $ namedGrid g
infos = M.fromList $ map (name . namedGrid &&& process) xs
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
-- | At this point, we have already ensured that orientations
-- redundant by rotational symmetry have been excluded
-- (i.e. at Scenario validation time).
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
where
definitionMap = M.fromList $ map (name &&& id) structDefs
definitionMap = M.fromList $ map ((name &&& id) . namedGrid) structDefs
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

View File

@ -0,0 +1,69 @@
{-# LANGUAGE OverloadedStrings #-}
-- |
-- SPDX-License-Identifier: BSD-3-Clause
--
-- Symmetry analysis for structure recognizer.
module Swarm.Game.Scenario.Topography.Structure.Recognition.Symmetry where
import Control.Monad (unless, when)
import Data.Map qualified as M
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 qualified as Structure
import Swarm.Game.Scenario.Topography.Structure.Recognition.Type (RotationalSymmetry (..), SymmetryAnnotatedGrid (..))
import Swarm.Language.Direction (AbsoluteDir (DSouth, DWest), getCoordinateOrientation)
import Swarm.Util (commaList, failT, histogram, showT)
-- | Warns if any recognition orientations are redundant
-- by rotational symmetry.
-- We can accomplish this by testing only two rotations:
--
-- 1. Rotate 90 degrees. If identical to the original
-- orientation, then has 4-fold symmetry and we don't
-- need to check any other orientations.
-- Warn if more than one recognition orientation was supplied.
-- 2. Rotate 180 degrees. At best, we may now have
-- 2-fold symmetry.
-- Warn if two opposite orientations were supplied.
checkSymmetry ::
(MonadFail m, Eq a) => Structure.NamedGrid a -> m (SymmetryAnnotatedGrid a)
checkSymmetry ng = do
case symmetryType of
FourFold ->
when (Set.size suppliedOrientations > 1)
. failT
. pure
$ T.unwords ["Redundant orientations supplied; with four-fold symmetry, just supply 'north'."]
TwoFold ->
unless (null redundantOrientations)
. failT
. pure
$ T.unwords
[ "Redundant"
, commaList $ map showT redundantOrientations
, "orientations supplied with two-fold symmetry."
]
where
redundantOrientations =
map fst
. filter ((> 1) . snd)
. M.toList
. histogram
. map getCoordinateOrientation
$ Set.toList suppliedOrientations
_ -> return ()
return $ SymmetryAnnotatedGrid ng symmetryType
where
symmetryType
| quarterTurnRows == originalRows = FourFold
| halfTurnRows == originalRows = TwoFold
| otherwise = NoSymmetry
quarterTurnRows = applyOrientationTransform (Orientation DWest False) originalRows
halfTurnRows = applyOrientationTransform (Orientation DSouth False) originalRows
suppliedOrientations = Structure.recognize ng
Structure.Grid originalRows = Structure.structure ng

View File

@ -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,13 +130,30 @@ 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)
data RotationalSymmetry
= -- | Aka 1-fold symmetry
NoSymmetry
| -- | Equivalent under rotation by 180 degrees
TwoFold
| -- | Equivalent under rotation by 90 degrees
FourFold
deriving (Show, Eq)
data SymmetryAnnotatedGrid a = SymmetryAnnotatedGrid
{ namedGrid :: NamedGrid a
, symmetry :: RotationalSymmetry
}
deriving (Show)
-- | Structure definitions with precomputed metadata for consumption by the UI
data StructureInfo = StructureInfo
{ withGrid :: StructureWithGrid
{ annotatedGrid :: SymmetryAnnotatedGrid (Maybe Cell)
, entityProcessedGrid :: [SymbolSequence]
, entityCounts :: Map Entity Int
}
@ -181,9 +199,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)

View File

@ -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 =

View File

@ -511,12 +511,12 @@ 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
`isJustOr` cmdExn Floorplan (pure $ T.unwords ["Unknown structure", quote name])
return . mkReturn . getAreaDimensions . entityGrid $ withGrid structureDef
return . mkReturn . getAreaDimensions $ entityProcessedGrid structureDef
_ -> badConst
HasTag -> case vs of
[VText eName, VText tName] -> do

View File

@ -16,6 +16,8 @@ module Swarm.Language.Direction (
directionSyntax,
isCardinal,
allDirs,
directionJsonModifier,
getCoordinateOrientation,
) where
import Data.Aeson.Types hiding (Key)
@ -51,6 +53,18 @@ data AbsoluteDir = DEast | DNorth | DWest | DSouth
directionJsonModifier :: String -> String
directionJsonModifier = map C.toLower . L.tail
data CoordinateOrientation
= Latitudinal
| Longitudinal
deriving (Show, Eq, Ord)
getCoordinateOrientation :: AbsoluteDir -> CoordinateOrientation
getCoordinateOrientation = \case
DEast -> Longitudinal
DWest -> Longitudinal
DNorth -> Latitudinal
DSouth -> Latitudinal
directionJsonOptions :: Options
directionJsonOptions =
defaultOptions

View File

@ -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

View File

@ -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

View File

@ -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")

View File

@ -16,6 +16,7 @@ import Brick.Widgets.List qualified as BL
import Control.Lens hiding (Const, from)
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 +29,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 (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)
-- | Render a two-pane widget with structure selection on the left
-- and single-structure details on the right.
@ -46,10 +49,10 @@ structureWidget gs s =
. T.pack
. renderRectDimensions
. getAreaDimensions
. entityGrid
$ withGrid s
$ entityProcessedGrid s
, occurrenceCountSuffix
]
, reorientabilityWidget
, maybeDescriptionWidget
, padTop (Pad 1) $
hBox
@ -64,7 +67,29 @@ structureWidget gs s =
, withAttr boldAttr $ txt content
]
maybeDescriptionWidget = maybe emptyWidget txtWrap $ Structure.description . originalDefinition . withGrid $ s
annotatedStructureGrid = annotatedGrid s
supportedOrientations = Set.toList . Structure.recognize . namedGrid $ annotatedStructureGrid
renderSymmetry = \case
NoSymmetry -> "no"
TwoFold -> "2-fold"
FourFold -> "4-fold"
reorientabilityWidget =
txt $
T.unwords
[ "Orientable:"
, commaList $ map (T.pack . directionJsonModifier . show) supportedOrientations
, "with"
, renderSymmetry $ symmetry annotatedStructureGrid
, "symmetry."
]
maybeDescriptionWidget =
maybe emptyWidget (padTop (Pad 1) . withAttr italicAttr . txtWrap) $
Structure.description . namedGrid . annotatedGrid $
s
registry = gs ^. discovery . structureRecognition . foundStructures
occurrenceCountSuffix = case M.lookup sName $ foundByName registry of
@ -72,7 +97,7 @@ structureWidget gs s =
Just inner -> padLeft (Pad 2) . headerItem "Count" . T.pack . show $ NEM.size inner
structureIllustration = vBox $ map (hBox . map renderOneCell) cells
d = originalDefinition $ withGrid s
d = namedGrid $ annotatedGrid s
ingredientsBox =
vBox
@ -97,8 +122,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 =
@ -140,5 +165,5 @@ drawSidebarListItem ::
Bool ->
StructureInfo ->
Widget Name
drawSidebarListItem _isSelected (StructureInfo swg _) =
txt . getStructureName . Structure.name $ originalDefinition swg
drawSidebarListItem _isSelected (StructureInfo annotated _ _) =
txt . getStructureName . Structure.name $ namedGrid annotated

View File

@ -192,6 +192,7 @@ library
Swarm.Game.Scenario.Topography.Structure.Recognition.Log
Swarm.Game.Scenario.Topography.Structure.Recognition.Precompute
Swarm.Game.Scenario.Topography.Structure.Recognition.Registry
Swarm.Game.Scenario.Topography.Structure.Recognition.Symmetry
Swarm.Game.Scenario.Topography.Structure.Recognition.Tracking
Swarm.Game.Scenario.Topography.Structure.Recognition.Type
Swarm.Game.Scenario.Topography.WorldDescription

View File

@ -236,6 +236,7 @@ testScenarioSolutions rs ui =
, testSolution (Sec 3) "Challenges/lights-out"
, testSolution (Sec 10) "Challenges/Sliding Puzzles/3x3"
, testSolution Default "Challenges/friend"
, testSolution Default "Challenges/pack-tetrominoes"
, testGroup
"Mazes"
[ testSolution Default "Challenges/Mazes/easy_cave_maze"
@ -428,6 +429,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