Skip to content

Commit

Permalink
Implement entity tags and commands (#1635)
Browse files Browse the repository at this point in the history
Closes #1631

## Design

* Entities have a new property: a `Set` of textual tags.
* Two new commands are introduced:
    * `HasTag` checks whether a single entity has a given tag
    * `TagMembers` allows cycling through all members with a given tag
* `TagMembers` may be considered more powerful than `HasTag`, so has its own separate capability (`CTagmembers`).
* A map is computed at scenario initialization to facilitate `TagMembers` lookups.
* Tag names are highlighted in yellow in markdown.

## Demo

    scripts/play.sh -i scenarios/Testing/1631-tags.yaml --autoplay

## Other changes

* Incidentally, changed `knownEntities` from a list to a `Set` so that `Set.member` can be used instead of `elem`.
  • Loading branch information
kostmo authored Nov 20, 2023
1 parent 4630e89 commit 37cae2a
Show file tree
Hide file tree
Showing 16 changed files with 222 additions and 14 deletions.
1 change: 1 addition & 0 deletions data/scenarios/Testing/00-ORDER.txt
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,5 @@ Achievements
1536-custom-unwalkable-entities.yaml
1535-ping
1575-structure-recognizer
1631-tags.yaml
1634-message-colors.yaml
130 changes: 130 additions & 0 deletions data/scenarios/Testing/1631-tags.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
version: 1
name: Test tag commands
description: |
Test the `hastag` and `tagmembers` command.
objectives:
- condition: |
as base {has "mushroom"}
prerequisite:
not: got_fruit
goal:
- |
Pick up something `edible`{=tag} that is not a `fruit`{=tag}.
- teaser: "No fruit!"
id: got_fruit
optional: true
condition: |
// Returns true if prohibited item is in inventory.
def checkFruit = \idx.
result <- tagmembers "fruit" idx;
let totalCount = fst result in
let member = snd result in
let nextIdx = idx + 1 in
hasProhibited <- as base {has member};
if hasProhibited {
return true;
} {
if (nextIdx < totalCount) {
checkFruit nextIdx;
} {
return false;
}
}
end;
checkFruit 0;
goal:
- |
Do not pick up any fruit.
solution: |
def findTarget =
result <- scan down;
isTarget <- case result (\_. return false) (\item.
isEdible <- hastag item "edible";
isFruit <- hastag item "fruit";
return $ isEdible && not isFruit;
);
if isTarget {
grab;
return ();
} {
move;
findTarget;
}
end;
findTarget;
robots:
- name: base
dir: [1,0]
devices:
- ADT calculator
- branch predictor
- barcode scanner
- dictionary
- grabber
- lambda
- lodestone
- logger
- scanner
- solar panel
- strange loop
- treads
entities:
- name: barcode scanner
display:
attr: red
char: 'S'
description:
- Reads the 'tag' of an item
properties: [portable]
capabilities: [hastag, tagmembers]
- name: canteloupe
display:
char: 'c'
description:
- Melon
tags: [edible, fruit]
properties: [portable]
- name: mushroom
display:
char: 'm'
description:
- Nature's tiny umbrella.
tags: [edible, fungus]
properties: [portable]
- name: gravel
display:
char: 'g'
description:
- Crushed rock
properties: [portable]
- name: strawberry
display:
char: 's'
description:
- Just ripe
tags: [edible, fruit]
- name: peach
display:
char: 'g'
description:
- Just ripe
tags: [edible, fruit]
properties: [portable]
world:
palette:
'.': [grass]
'B': [grass, null, base]
'a': [grass, canteloupe]
'b': [grass, gravel]
'c': [grass, strawberry]
'd': [grass, mushroom]
'e': [grass, peach]
upperleft: [-5, 5]
map: |
.......
B.abcde
.......
7 changes: 7 additions & 0 deletions data/schema/entity.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@
},
"description": "A description of the entity, as a list of paragraphs."
},
"tags": {
"type": "array",
"items": {
"type": "string"
},
"description": "A list of categories this entity belongs to."
},
"orientation": {
"default": null,
"type": "array",
Expand Down
2 changes: 2 additions & 0 deletions editors/emacs/swarm-mode.el
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@
"waypoint"
"structure"
"floorplan"
"hastag"
"tagmembers"
"detect"
"resonate"
"density"
Expand Down
2 changes: 1 addition & 1 deletion editors/vim/swarm.vim
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
syn keyword Keyword def end let in require
syn keyword Builtins self parent base if inl inr case fst snd force undefined fail not format chars split charat tochar key
syn keyword Command noop wait selfdestruct move backup path push stride turn grab harvest ignite place ping give equip unequip make has equipped count drill use build salvage reprogram say listen log view appear create halt time scout whereami waypoint structure floorplan detect resonate density sniff chirp watch surveil heading blocked scan upload ishere isempty meet meetall whoami setname random run return try swap atomic instant installkeyhandler teleport as robotnamed robotnumbered knows
syn keyword Command noop wait selfdestruct move backup path push stride turn grab harvest ignite place ping give equip unequip make has equipped count drill use build salvage reprogram say listen log view appear create halt time scout whereami waypoint structure floorplan hastag tagmembers detect resonate density sniff chirp watch surveil heading blocked scan upload ishere isempty meet meetall whoami setname random run return try swap atomic instant installkeyhandler teleport as robotnamed robotnumbered knows
syn keyword Direction east north west south down forward left back right
syn keyword Type int text dir bool cmd void unit actor

Expand Down
2 changes: 1 addition & 1 deletion editors/vscode/syntaxes/swarm.tmLanguage.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@
},
{
"name": "keyword.other",
"match": "\\b(?i)(self|parent|base|if|inl|inr|case|fst|snd|force|undefined|fail|not|format|chars|split|charat|tochar|key|noop|wait|selfdestruct|move|backup|path|push|stride|turn|grab|harvest|ignite|place|ping|give|equip|unequip|make|has|equipped|count|drill|use|build|salvage|reprogram|say|listen|log|view|appear|create|halt|time|scout|whereami|waypoint|structure|floorplan|detect|resonate|density|sniff|chirp|watch|surveil|heading|blocked|scan|upload|ishere|isempty|meet|meetall|whoami|setname|random|run|return|try|swap|atomic|instant|installkeyhandler|teleport|as|robotnamed|robotnumbered|knows)\\b"
"match": "\\b(?i)(self|parent|base|if|inl|inr|case|fst|snd|force|undefined|fail|not|format|chars|split|charat|tochar|key|noop|wait|selfdestruct|move|backup|path|push|stride|turn|grab|harvest|ignite|place|ping|give|equip|unequip|make|has|equipped|count|drill|use|build|salvage|reprogram|say|listen|log|view|appear|create|halt|time|scout|whereami|waypoint|structure|floorplan|hastag|tagmembers|detect|resonate|density|sniff|chirp|watch|surveil|heading|blocked|scan|upload|ishere|isempty|meet|meetall|whoami|setname|random|run|return|try|swap|atomic|instant|installkeyhandler|teleport|as|robotnamed|robotnumbered|knows)\\b"
}
]
},
Expand Down
15 changes: 13 additions & 2 deletions src/Swarm/Game/Entity.hs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ module Swarm.Game.Entity (
entityPlural,
entityNameFor,
entityDescription,
entityTags,
entityOrientation,
entityGrowth,
entityCombustion,
Expand Down Expand Up @@ -255,6 +256,8 @@ data Entity = Entity
, _entityDescription :: Document Syntax
-- ^ A longer-form description. Each 'Text' value is one
-- paragraph.
, _entityTags :: Set Text
-- ^ A set of categories to which the entity belongs
, _entityOrientation :: Maybe Heading
-- ^ The entity's orientation (if it has one). For example, when
-- a robot moves, it moves in the direction of its orientation.
Expand All @@ -281,12 +284,13 @@ data Entity = Entity
-- | The @Hashable@ instance for @Entity@ ignores the cached hash
-- value and simply combines the other fields.
instance Hashable Entity where
hashWithSalt s (Entity _ disp nm pl descr orient grow combust yld props caps inv) =
hashWithSalt s (Entity _ disp nm pl descr tags orient grow combust yld props caps inv) =
s
`hashWithSalt` disp
`hashWithSalt` nm
`hashWithSalt` pl
`hashWithSalt` docToText descr
`hashWithSalt` tags
`hashWithSalt` orient
`hashWithSalt` grow
`hashWithSalt` combust
Expand Down Expand Up @@ -330,6 +334,7 @@ mkEntity disp nm descr props caps =
nm
Nothing
descr
mempty
Nothing
Nothing
Nothing
Expand Down Expand Up @@ -394,7 +399,8 @@ instance FromJSON Entity where
<$> v .: "display"
<*> v .: "name"
<*> v .:? "plural"
<*> (v .: "description")
<*> v .: "description"
<*> v .:? "tags" .!= mempty
<*> v .:? "orientation"
<*> v .:? "growth"
<*> v .:? "combustion"
Expand All @@ -418,6 +424,7 @@ instance ToJSON Entity where
[ "display" .= (e ^. entityDisplay)
, "name" .= (e ^. entityName)
, "description" .= (e ^. entityDescription)
, "tags" .= (e ^. entityTags)
]
++ ["plural" .= (e ^. entityPlural) | isJust (e ^. entityPlural)]
++ ["orientation" .= (e ^. entityOrientation) | isJust (e ^. entityOrientation)]
Expand Down Expand Up @@ -490,6 +497,10 @@ entityNameFor _ = to $ \e ->
entityDescription :: Lens' Entity (Document Syntax)
entityDescription = hashedLens _entityDescription (\e x -> e {_entityDescription = x})

-- | A set of categories to which the entity belongs
entityTags :: Lens' Entity (Set Text)
entityTags = hashedLens _entityTags (\e x -> e {_entityTags = x})

-- | The direction this entity is facing (if it has one).
entityOrientation :: Lens' Entity (Maybe Heading)
entityOrientation = hashedLens _entityOrientation (\e x -> e {_entityOrientation = x})
Expand Down
10 changes: 6 additions & 4 deletions src/Swarm/Game/Scenario.hs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ import Data.List.NonEmpty qualified as NE
import Data.Map qualified as M
import Data.Maybe (catMaybes, isNothing, listToMaybe)
import Data.Sequence (Seq)
import Data.Set (Set)
import Data.Set qualified as Set
import Data.Text (Text)
import Data.Text qualified as T
import Swarm.Game.Entity
Expand Down Expand Up @@ -126,7 +128,7 @@ data Scenario = Scenario
, _scenarioAttrs :: [CustomAttr]
, _scenarioEntities :: EntityMap
, _scenarioRecipes :: [Recipe Entity]
, _scenarioKnown :: [Text]
, _scenarioKnown :: Set EntityName
, _scenarioWorlds :: NonEmpty WorldDescription
, _scenarioNavigation :: Navigation (M.Map SubworldName) Location
, _scenarioStructures :: StaticStructureInfo
Expand Down Expand Up @@ -154,7 +156,7 @@ instance FromJSONE (EntityMap, WorldMap) Scenario where
-- with any custom entities parsed above
localE fst $ withE em $ do
-- parse 'known' entity names and make sure they exist
known <- liftE (v .:? "known" .!= [])
known <- liftE (v .:? "known" .!= mempty)
em' <- getE
case filter (isNothing . (`lookupEntityName` em')) known of
[] -> return ()
Expand Down Expand Up @@ -227,7 +229,7 @@ instance FromJSONE (EntityMap, WorldMap) Scenario where
<*> liftE (v .:? "attrs" .!= [])
<*> pure em
<*> v ..:? "recipes" ..!= []
<*> pure known
<*> pure (Set.fromList known)
<*> pure allWorlds
<*> pure mergedNavigation
<*> pure structureInfo
Expand Down Expand Up @@ -273,7 +275,7 @@ scenarioRecipes :: Lens' Scenario [Recipe Entity]

-- | List of entities that should be considered "known", so robots do
-- not have to scan them.
scenarioKnown :: Lens' Scenario [Text]
scenarioKnown :: Lens' Scenario (Set EntityName)

-- | The subworlds of the scenario.
-- The "root" subworld shall always be at the head of the list, by construction.
Expand Down
20 changes: 17 additions & 3 deletions src/Swarm/Game/State.hs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ module Swarm.Game.State (
knownEntities,
gameAchievements,
structureRecognition,
tagMembers,

-- *** Landscape
Landscape,
Expand Down Expand Up @@ -525,9 +526,10 @@ data Discovery = Discovery
{ _allDiscoveredEntities :: Inventory
, _availableRecipes :: Notifications (Recipe Entity)
, _availableCommands :: Notifications Const
, _knownEntities :: [Text]
, _knownEntities :: S.Set EntityName
, _gameAchievements :: Map GameplayAchievement Attainment
, _structureRecognition :: StructureRecognizer
, _tagMembers :: Map Text (NonEmpty EntityName)
}

makeLensesNoSigs ''Discovery
Expand All @@ -543,14 +545,17 @@ availableCommands :: Lens' Discovery (Notifications Const)

-- | The names of entities that should be considered \"known\", that is,
-- robots know what they are without having to scan them.
knownEntities :: Lens' Discovery [Text]
knownEntities :: Lens' Discovery (S.Set EntityName)

-- | Map of in-game achievements that were obtained
gameAchievements :: Lens' Discovery (Map GameplayAchievement Attainment)

-- | Recognizer for robot-constructed structures
structureRecognition :: Lens' Discovery StructureRecognizer

-- | Map from tags to entities that possess that tag
tagMembers :: Lens' Discovery (Map Text (NonEmpty EntityName))

data Landscape = Landscape
{ _worldNavigation :: Navigation (M.Map SubworldName) Location
, _multiWorld :: W.MultiWorld Int Entity
Expand Down Expand Up @@ -1185,11 +1190,12 @@ initGameState gsc =
{ _availableRecipes = mempty
, _availableCommands = mempty
, _allDiscoveredEntities = empty
, _knownEntities = []
, _knownEntities = mempty
, -- 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) emptyFoundStructures []
, _tagMembers = mempty
}
, _activeRobots = IS.empty
, _waitingRobots = M.empty
Expand Down Expand Up @@ -1349,6 +1355,13 @@ mkRecognizer structInfo@(StaticStructureInfo structDefs _) = do
where
allPlaced = lookupStaticPlacements structInfo

buildTagMap :: EntityMap -> Map Text (NonEmpty EntityName)
buildTagMap em =
binTuples expanded
where
expanded = concatMap (\(k, vs) -> [(v, k) | v <- S.toList vs]) $ M.toList tagsByEntity
tagsByEntity = M.map (view entityTags) $ entitiesByName em

pureScenarioToGameState ::
Scenario ->
Seed ->
Expand Down Expand Up @@ -1377,6 +1390,7 @@ pureScenarioToGameState scenario theSeed now toRun gsc =
& internalActiveRobots .~ setOf (traverse . robotID) robotList'
& discovery . availableCommands .~ Notifications 0 initialCommands
& discovery . knownEntities .~ scenario ^. scenarioKnown
& discovery . tagMembers .~ buildTagMap em
& robotNaming . gensym .~ initGensym
& seed .~ theSeed
& randGen .~ mkStdGen theSeed
Expand Down
15 changes: 15 additions & 0 deletions src/Swarm/Game/Step.hs
Original file line number Diff line number Diff line change
Expand Up @@ -1443,6 +1443,21 @@ execConst c vs s k = do
`isJustOr` cmdExn Floorplan (pure $ T.unwords ["Unknown structure", quote name])
return . mkReturn . getAreaDimensions . entityGrid $ withGrid structureDef
_ -> badConst
HasTag -> case vs of
[VText eName, VText tName] -> do
em <- use $ landscape . entityMap
e <-
lookupEntityName eName em
`isJustOrFail` ["I've never heard of", indefiniteQ eName <> "."]
return $ mkReturn $ tName `S.member` (e ^. entityTags)
_ -> badConst
TagMembers -> case vs of
[VText tagName, VInt idx] -> do
tm <- use $ discovery . tagMembers
case M.lookup tagName tm of
Nothing -> throwError $ CmdFailed TagMembers (T.unwords ["No tag named", tagName]) Nothing
Just theMembers -> return $ mkReturn (NE.length theMembers, indexWrapNonEmpty theMembers idx)
_ -> badConst
Detect -> case vs of
[VText name, VRect x1 y1 x2 y2] -> do
loc <- use robotLocation
Expand Down
Loading

0 comments on commit 37cae2a

Please sign in to comment.