diff --git a/data/scenarios/Testing/1356-portals/00-ORDER.txt b/data/scenarios/Testing/1356-portals/00-ORDER.txt new file mode 100644 index 0000000000..905ba32c7e --- /dev/null +++ b/data/scenarios/Testing/1356-portals/00-ORDER.txt @@ -0,0 +1,2 @@ +portals-and-waypoints.yaml +portals-flip-and-rotate.yaml \ No newline at end of file diff --git a/data/scenarios/Testing/1356-portals/_automatic-waypoint-patrol/program.sw b/data/scenarios/Testing/1356-portals/_automatic-waypoint-patrol/program.sw new file mode 100644 index 0000000000..8cf22bf374 --- /dev/null +++ b/data/scenarios/Testing/1356-portals/_automatic-waypoint-patrol/program.sw @@ -0,0 +1,49 @@ +def doN = \n. \f. if (n > 0) {f; doN (n - 1) f} {}; end; +def abs = \n. if (n < 0) {-n} {n} end; + +def mapTuple = \f. \t. + (f $ fst t, f $ snd t) + end; + +def sumTuples = \t1. \t2. + (fst t1 + fst t2, snd t1 + snd t2); + end; + +def negateTuple = \t. + mapTuple (\x. -x) t; + end; + +def getRelativeLocation = \absCurrentLoc. \absDestLoc. + let negatedLoc = negateTuple absCurrentLoc in + return $ sumTuples negatedLoc absDestLoc; + end; + +def moveTuple = \tup. + let x = fst tup in + let y = snd tup in + turn $ if (x > 0) {east} {west}; + doN (abs x) move; + turn $ if (y > 0) {north} {south}; + doN (abs y) move; + end; + +def goToLocation = \currentLoc. \absoluteDestination. + relativeDestination <- getRelativeLocation currentLoc absoluteDestination; + moveTuple relativeDestination; + end; + +def visitNextWaypoint = \nextWpIdx. + loc <- whereami; + nextWaypointQuery <- waypoint "wp" nextWpIdx; + goToLocation loc $ snd nextWaypointQuery; + + visitNextWaypoint $ nextWpIdx + 1; + end; + +def go = + waypointQuery <- waypoint "wp" 0; + teleport self $ snd waypointQuery; + visitNextWaypoint 1; + end; + +go; diff --git a/data/scenarios/Testing/1356-portals/_portals-flip-and-rotate/solution.sw b/data/scenarios/Testing/1356-portals/_portals-flip-and-rotate/solution.sw new file mode 100644 index 0000000000..fece18bb29 --- /dev/null +++ b/data/scenarios/Testing/1356-portals/_portals-flip-and-rotate/solution.sw @@ -0,0 +1,61 @@ +def doN = \n. \f. if (n > 0) {f; doN (n - 1) f} {}; end; + +def abs = \n. if (n < 0) {-n} {n} end; + +def mapTuple = \f. \t. + (f $ fst t, f $ snd t) + end; + +def sumTuples = \t1. \t2. + (fst t1 + fst t2, snd t1 + snd t2); + end; + +def negateTuple = \t. + mapTuple (\x. -x) t; + end; + +def getRelativeLocation = \absCurrentLoc. \absDestLoc. + let negatedLoc = negateTuple absCurrentLoc in + return $ sumTuples negatedLoc absDestLoc; + end; + +def moveTuple = \tup. + let x = fst tup in + let y = snd tup in + turn $ if (x > 0) {east} {west}; + doN (abs x) move; + turn $ if (y > 0) {north} {south}; + doN (abs y) move; + end; + +def goToLocation = \currentLoc. \absoluteDestination. + relativeDestination <- getRelativeLocation currentLoc absoluteDestination; + moveTuple relativeDestination; + end; + +def goToBottom = + turn south; doN 14 move; + end; + +def go = + goToLocation (0, 0) (3, -2); + goToLocation (0, 0) (12, -2); + goToLocation (0, 0) (18, -5); + goToLocation (0, 0) (23, -3); + + goToBottom; + goToLocation (0, -14) (3, -12); + goToBottom; + goToLocation (0, -14) (9, -9); + goToBottom; + goToLocation (0, -14) (18, -9); + goToBottom; + goToLocation (0, -14) (26, -10); + + turn east; + doN 29 move; + goToBottom; + grab; + end; + +go; \ No newline at end of file diff --git a/data/scenarios/Testing/1356-portals/automatic-waypoint-patrol.yaml b/data/scenarios/Testing/1356-portals/automatic-waypoint-patrol.yaml new file mode 100644 index 0000000000..90924df3bd --- /dev/null +++ b/data/scenarios/Testing/1356-portals/automatic-waypoint-patrol.yaml @@ -0,0 +1,73 @@ +version: 1 +name: Querying waypoints +description: | + Demonstrate patrolling between waypoints +creative: true +robots: + - name: base + loc: [0, 0] + dir: [1, 0] + - name: patroller + loc: [5, -4] + dir: [1, 0] + display: + invisible: false + attr: robot + program: | + run "scenarios/Testing/1356-portals/_automatic-waypoint-patrol/program.sw" +known: [flower, boulder] +world: + upperleft: [-1, 1] + default: [blank] + palette: + '.': [grass] + '*': [stone, flower] + '┌': [stone, upper left corner] + '┐': [stone, upper right corner] + '└': [stone, lower left corner] + '┘': [stone, lower right corner] + '─': [stone, horizontal wall] + '│': [stone, vertical wall] + structures: + - name: bigbox + structure: + palette: + '.': [dirt] + '@': [dirt, boulder] + 'w': + cell: [dirt] + waypoint: + name: wp + map: | + @@@ + @w. + @.@ + placements: + - src: bigbox + offset: [2, -2] + orient: + up: "DNorth" + - src: bigbox + offset: [8, -2] + orient: + up: "DEast" + - src: bigbox + offset: [8, -6] + orient: + up: "DSouth" + - src: bigbox + offset: [2, -6] + orient: + up: "DWest" + map: | + ┌───────────┐ + │*..*..*..*.│ + │.*..*..*..*│ + │..*..*..*..│ + │*..*..*..*.│ + │.*..*..*..*│ + │..*..*..*..│ + │*..*..*..*.│ + │.*..*..*..*│ + │..*..*..*..│ + └───────────┘ diff --git a/data/scenarios/Testing/1356-portals/portals-and-waypoints.yaml b/data/scenarios/Testing/1356-portals/portals-and-waypoints.yaml new file mode 100644 index 0000000000..95f24e3cb3 --- /dev/null +++ b/data/scenarios/Testing/1356-portals/portals-and-waypoints.yaml @@ -0,0 +1,115 @@ +version: 1 +name: Waypoints for nested structures +description: | + Demonstrate behavior of waypoints across structure overlays +creative: true +attrs: + - name: portal_in + fg: "#ff9a00" + bg: "#ff5d00" + - name: portal_out + fg: "#00a2ff" + bg: "#0065ff" +entities: + - name: telepad entrance + display: + attr: portal_in + char: "o" + description: + - Portal entrance + properties: [known] + - name: telepad exit + display: + attr: portal_out + char: "o" + description: + - Portal exit + properties: [known] +robots: + - name: base + loc: [0, 4] + dir: [1, 0] +known: [tree, flower, sand, bit (0), bit (1)] +world: + upperleft: [-4, 7] + default: [blank] + palette: + '.': [grass] + '*': [stone, flower] + 'P': [grass, telepad entrance] + 'p': [grass, telepad exit] + '┌': [stone, upper left corner] + '┐': [stone, upper right corner] + '└': [stone, lower left corner] + '┘': [stone, lower right corner] + '─': [stone, horizontal wall] + '│': [stone, vertical wall] + structures: + - name: bitpair + structure: + palette: + 'p': [stone, telepad exit] + '1': [stone, bit (1)] + map: | + 1 + p + waypoints: + - name: bitpair_bottom + loc: [0, -1] + - name: minibox + structure: + palette: + '.': [stone] + 's': [stone, sand] + 'P': [stone, telepad entrance] + placements: + - src: bitpair + offset: [1, 0] + waypoints: + - name: minibox_corner + loc: [0, 0] + map: | + P.s + s.s + - name: bigbox + structure: + palette: + '.': [stone] + 'T': [stone, tree] + 'w': + cell: [dirt, telepad entrance] + waypoint: + name: bigbox_middle + map: | + TTTTTT + T.TwT. + .T.T.T + TTTTTT + placements: + - src: bigbox + offset: [1, -1] + - src: bigbox + offset: [7, -5] + - src: minibox + offset: [1, -7] + waypoints: + - name: meadow + loc: [12, -1] + portals: + - entrance: bigbox_middle + exitInfo: + exit: bitpair_bottom + - entrance: minibox_corner + exitInfo: + exit: meadow + map: | + ┌────────────┐ + │*..*..*..*.p│ + │.*..*..*..*.│ + │..*..*..*..*│ + │*..*..*..*..│ + │.*..*..*..*.│ + │..*..*..*..*│ + │*..*..*..*..│ + │.*..*..*..*.│ + └────────────┘ diff --git a/data/scenarios/Testing/1356-portals/portals-flip-and-rotate.yaml b/data/scenarios/Testing/1356-portals/portals-flip-and-rotate.yaml new file mode 100644 index 0000000000..de142e303a --- /dev/null +++ b/data/scenarios/Testing/1356-portals/portals-flip-and-rotate.yaml @@ -0,0 +1,143 @@ +version: 1 +name: Portals with substructure flip and rotation +description: | + Validate proper flip/rotate of portal waypoints +objectives: + - goal: + - | + `grab` the "bitcoin" + condition: | + as base {has "bitcoin"} +solution: | + run "scenarios/Testing/1356-portals/_portals-flip-and-rotate/solution.sw" +attrs: + - name: portal_in + fg: "#ff9a00" + bg: "#ff5d00" + - name: portal_out + fg: "#00a2ff" + bg: "#0065ff" +entities: + - name: telepad entrance + display: + attr: portal_in + char: "o" + description: + - Portal entrance + properties: [known] + - name: telepad exit + display: + attr: portal_out + char: "o" + description: + - Portal exit + properties: [known] +robots: + - name: base + dir: [1, 0] + devices: + - ADT calculator + - branch predictor + - comparator + - compass + - dictionary + - GPS receiver + - grabber + - lambda + - lodestone + - logger + - strange loop + - treads +known: [flower, bit (0), bit (1), bitcoin] +world: + default: [blank] + palette: + '.': [grass] + '*': [stone, flower] + 'b': [stone, bitcoin] + '┌': [stone, upper left corner] + '┐': [stone, upper right corner] + '└': [stone, lower left corner] + '┘': [stone, lower right corner] + '─': [stone, horizontal wall] + '│': [stone, vertical wall] + 'p': + cell: [dirt, telepad exit, base] + waypoint: + name: portal_out + upperleft: [-1, 1] + structures: + - name: tetromino + structure: + mask: '.' + palette: + '0': [stone, bit (0)] + '1': [stone, bit (1)] + 'P': + cell: [dirt, telepad entrance] + waypoint: + name: portal_in + map: | + 10.. + 1P.. + 10.. + 10.. + 1000 + 1111 + placements: + - src: tetromino + offset: [3, -2] + - src: tetromino + offset: [9, -2] + orient: + up: "DEast" + - src: tetromino + offset: [17, -2] + orient: + up: "DSouth" + - src: tetromino + offset: [23, -2] + orient: + up: "DWest" + - src: tetromino + offset: [3, -9] + orient: + up: "DNorth" + flip: true + - src: tetromino + offset: [9, -9] + orient: + up: "DEast" + flip: true + - src: tetromino + offset: [17, -9] + orient: + up: "DSouth" + flip: true + - src: tetromino + offset: [23, -9] + orient: + up: "DWest" + flip: true + portals: + - entrance: portal_in + exitInfo: + exit: portal_out + map: | + ┌──────────────────────────────┐ + │p..*..*..*..*..*..*..*..*..*..│ + │.*..*..*..*..*..*..*..*..*..*.│ + │..*..*..*..*..*..*..*..*..*..*│ + │*..*..*..*..*..*..*..*..*..*..│ + │.*..*..*..*..*..*..*..*..*..*.│ + │..*..*..*..*..*..*..*..*..*..*│ + │*..*..*..*..*..*..*..*..*..*..│ + │.*..*..*..*..*..*..*..*..*..*.│ + │..*..*..*..*..*..*..*..*..*..*│ + │*..*..*..*..*..*..*..*..*..*..│ + │.*..*..*..*..*..*..*..*..*..*.│ + │..*..*..*..*..*..*..*..*..*..*│ + │*..*..*..*..*..*..*..*..*..*..│ + │.*..*..*..*..*..*..*..*..*..*.│ + │..*..*..*..*..*..*..*..*..*..b│ + └──────────────────────────────┘ diff --git a/data/scenarios/Testing/_Validation/1356-ambiguous-portal-entrance.yaml b/data/scenarios/Testing/_Validation/1356-ambiguous-portal-entrance.yaml new file mode 100644 index 0000000000..efab55c981 --- /dev/null +++ b/data/scenarios/Testing/_Validation/1356-ambiguous-portal-entrance.yaml @@ -0,0 +1,67 @@ +version: 1 +name: Reject multi-exit portal +description: | + Portals must have only a single exit +attrs: + - name: portal_in + fg: "#ff9a00" + bg: "#ff5d00" + - name: portal_out + fg: "#00a2ff" + bg: "#0065ff" +entities: + - name: telepad entrance + display: + attr: portal_in + char: "o" + description: + - Portal entrance + properties: [known] + - name: telepad exit + display: + attr: portal_out + char: "o" + description: + - Portal exit + properties: [known] +robots: + - name: base + dir: [1, 0] +known: [tree] +world: + upperleft: [-1, 1] + default: [blank] + palette: + '.': [grass] + 'B': [grass, null, base] + 'P': + cell: [grass, telepad entrance] + waypoint: + name: inportal + 'p': + cell: [grass, telepad exit] + waypoint: + name: outportal1 + 'q': + cell: [grass, telepad exit] + waypoint: + name: outportal2 + '┌': [stone, upper left corner] + '┐': [stone, upper right corner] + '└': [stone, lower left corner] + '┘': [stone, lower right corner] + '─': [stone, horizontal wall] + '│': [stone, vertical wall] + portals: + - entrance: inportal + exitInfo: + exit: outportal1 + - entrance: inportal + exitInfo: + exit: outportal2 + map: | + ┌────────┐ + │....B..q│ + │.p......│ + │....P...│ + └────────┘ diff --git a/data/scenarios/Testing/_Validation/1356-ambiguous-portal-exit.yaml b/data/scenarios/Testing/_Validation/1356-ambiguous-portal-exit.yaml new file mode 100644 index 0000000000..0b104636ff --- /dev/null +++ b/data/scenarios/Testing/_Validation/1356-ambiguous-portal-exit.yaml @@ -0,0 +1,60 @@ +version: 1 +name: Reject overlapping portal entrances +description: | + Two portals must not share the same entrance location +attrs: + - name: portal_in + fg: "#ff9a00" + bg: "#ff5d00" + - name: portal_out + fg: "#00a2ff" + bg: "#0065ff" +entities: + - name: telepad entrance + display: + attr: portal_in + char: "o" + description: + - Portal entrance + properties: [known] + - name: telepad exit + display: + attr: portal_out + char: "o" + description: + - Portal exit + properties: [known] +robots: + - name: base + dir: [1, 0] +known: [tree] +world: + upperleft: [1, -1] + default: [blank] + palette: + '.': [grass] + 'B': [grass, null, base] + 'P': + cell: [grass, telepad entrance] + waypoint: + name: inportal + 'p': + cell: [grass, telepad exit] + waypoint: + name: outportal + '┌': [stone, upper left corner] + '┐': [stone, upper right corner] + '└': [stone, lower left corner] + '┘': [stone, lower right corner] + '─': [stone, horizontal wall] + '│': [stone, vertical wall] + portals: + - entrance: inportal + exitInfo: + exit: outportal + map: | + ┌────────┐ + │....B..p│ + │.p......│ + │....P...│ + └────────┘ diff --git a/data/scenarios/Testing/_Validation/1356-waypoint-uniqueness-enforcement.yaml b/data/scenarios/Testing/_Validation/1356-waypoint-uniqueness-enforcement.yaml new file mode 100644 index 0000000000..032f2457b1 --- /dev/null +++ b/data/scenarios/Testing/_Validation/1356-waypoint-uniqueness-enforcement.yaml @@ -0,0 +1,43 @@ +version: 1 +name: Waypoint uniqueness enforcement +description: | + Waypoints can optionally be required to be unique +attrs: + - name: portal_out + fg: "#00a2ff" + bg: "#0065ff" +entities: + - name: telepad exit + display: + attr: portal_out + char: "o" + description: + - Portal exit + properties: [known] +robots: + - name: base + dir: [1, 0] +known: [tree] +world: + upperleft: [1, -1] + default: [blank] + palette: + '.': [grass] + 'B': [grass, null, base] + 'p': + cell: [grass, telepad exit] + waypoint: + name: outportal + unique: True + '┌': [stone, upper left corner] + '┐': [stone, upper right corner] + '└': [stone, lower left corner] + '┘': [stone, lower right corner] + '─': [stone, horizontal wall] + '│': [stone, vertical wall] + map: | + ┌────────┐ + │....B..p│ + │.p......│ + │........│ + └────────┘ diff --git a/editors/emacs/swarm-mode.el b/editors/emacs/swarm-mode.el index 45628c1fbb..79356b221e 100644 --- a/editors/emacs/swarm-mode.el +++ b/editors/emacs/swarm-mode.el @@ -82,6 +82,7 @@ "time" "scout" "whereami" + "waypoint" "detect" "resonate" "density" diff --git a/editors/vscode/syntaxes/swarm.tmLanguage.json b/editors/vscode/syntaxes/swarm.tmLanguage.json index 63247768eb..6a6e4e1a53 100644 --- a/editors/vscode/syntaxes/swarm.tmLanguage.json +++ b/editors/vscode/syntaxes/swarm.tmLanguage.json @@ -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|push|stride|turn|grab|harvest|place|give|equip|unequip|make|has|equipped|count|drill|use|build|salvage|reprogram|say|listen|log|view|appear|create|halt|time|scout|whereami|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|push|stride|turn|grab|harvest|place|give|equip|unequip|make|has|equipped|count|drill|use|build|salvage|reprogram|say|listen|log|view|appear|create|halt|time|scout|whereami|waypoint|detect|resonate|density|sniff|chirp|watch|surveil|heading|blocked|scan|upload|ishere|isempty|meet|meetall|whoami|setname|random|run|return|try|swap|atomic|instant|installkeyhandler|teleport|as|robotnamed|robotnumbered|knows)\\b" } ] }, diff --git a/src/Swarm/Game/Scenario/Cell.hs b/src/Swarm/Game/Scenario/Cell.hs index 14c527c2e9..afd936e817 100644 --- a/src/Swarm/Game/Scenario/Cell.hs +++ b/src/Swarm/Game/Scenario/Cell.hs @@ -6,6 +6,7 @@ module Swarm.Game.Scenario.Cell ( PCell (..), Cell, + AugmentedCell (..), CellPaintDisplay, ) where @@ -16,9 +17,10 @@ import Data.Maybe (catMaybes, listToMaybe) import Data.Text (Text) import Data.Vector qualified as V import Data.Yaml as Y -import Swarm.Game.Entity +import Swarm.Game.Entity hiding (empty) import Swarm.Game.Scenario.EntityFacade import Swarm.Game.Scenario.RobotLookup +import Swarm.Game.Scenario.Waypoint (WaypointConfig) import Swarm.Game.Terrain import Swarm.Util.Yaml @@ -41,6 +43,12 @@ data PCell e = Cell -- and optionally an entity and robot. type Cell = PCell Entity +data AugmentedCell e = AugmentedCell + { waypointCfg :: Maybe WaypointConfig + , standardCell :: PCell e + } + deriving (Eq, Show) + -- | Re-usable serialization for variants of "PCell" mkPCellJson :: ToJSON b => (a -> b) -> PCell a -> Value mkPCellJson modifier x = @@ -54,10 +62,6 @@ mkPCellJson modifier x = instance ToJSON Cell where toJSON = mkPCellJson $ view entityName --- | Parse a tuple such as @[grass, rock, base]@ into a 'Cell'. The --- entity and robot, if present, are immediately looked up and --- converted into 'Entity' and 'TRobot' values. If they are not --- found, a parse error results. instance FromJSONE (EntityMap, RobotMap) Cell where parseJSONE = withArrayE "tuple" $ \v -> do let tup = V.toList v @@ -79,6 +83,20 @@ instance FromJSONE (EntityMap, RobotMap) Cell where return $ Cell terr ent robs +-- | Parse a tuple such as @[grass, rock, base]@ into a 'Cell'. The +-- entity and robot, if present, are immediately looked up and +-- converted into 'Entity' and 'TRobot' values. If they are not +-- found, a parse error results. +instance FromJSONE (EntityMap, RobotMap) (AugmentedCell Entity) where + parseJSONE x = case x of + Object v -> objParse v + z -> AugmentedCell Nothing <$> parseJSONE z + where + objParse v = do + wp <- liftE $ v .:? "waypoint" + origCell <- v ..: "cell" + return $ AugmentedCell wp origCell + ------------------------------------------------------------ -- World editor ------------------------------------------------------------ diff --git a/src/Swarm/Game/Scenario/Portal.hs b/src/Swarm/Game/Scenario/Portal.hs new file mode 100644 index 0000000000..67021fb08e --- /dev/null +++ b/src/Swarm/Game/Scenario/Portal.hs @@ -0,0 +1,26 @@ +-- | +-- SPDX-License-Identifier: BSD-3-Clause +module Swarm.Game.Scenario.Portal where + +import Data.Aeson +import Data.Text (Text) +import GHC.Generics (Generic) +import Swarm.Game.Scenario.Waypoint + +-- | Note: The primary overworld shall use +-- the reserved name \"root\". +newtype SubworldName = SubworldName Text + deriving (Show, Eq, Ord, Generic, FromJSON) + +data PortalExit = PortalExit + { exit :: WaypointName + , subworldName :: Maybe SubworldName + -- ^ Note: 'Nothing' indicates that references a waypoint within the same subworld. + } + deriving (Show, Eq, Generic, FromJSON) + +data Portal = Portal + { entrance :: WaypointName + , exitInfo :: PortalExit + } + deriving (Show, Eq, Generic, FromJSON) diff --git a/src/Swarm/Game/Scenario/Structure.hs b/src/Swarm/Game/Scenario/Structure.hs index 6bdb53060b..bd9289ca7b 100644 --- a/src/Swarm/Game/Scenario/Structure.hs +++ b/src/Swarm/Game/Scenario/Structure.hs @@ -8,9 +8,10 @@ import Control.Applicative ((<|>)) import Control.Arrow ((&&&)) import Data.Aeson.Key qualified as Key import Data.Aeson.KeyMap qualified as KeyMap +import Data.Coerce import Data.List (transpose) import Data.Map qualified as M -import Data.Maybe (mapMaybe) +import Data.Maybe (catMaybes, mapMaybe) import Data.Text (Text) import Data.Text qualified as T import Data.Yaml as Y @@ -19,11 +20,21 @@ import Swarm.Game.Entity import Swarm.Game.Location import Swarm.Game.Scenario.Cell import Swarm.Game.Scenario.RobotLookup +import Swarm.Game.Scenario.Waypoint import Swarm.Game.Scenario.WorldPalette import Swarm.Language.Syntax (AbsoluteDir (..)) +import Swarm.TUI.Editor.Area import Swarm.Util.Yaml import Witch (into) +-- | Indicates which structure something came from +-- for debugging purposes. +data Originated a = Originated + { parent :: Maybe Placement + , value :: a + } + deriving (Show, Eq, Functor) + newtype StructureName = StructureName Text deriving (Eq, Ord, Show, Generic, FromJSON) @@ -46,13 +57,16 @@ data PStructure c = Structure -- ^ structure definitions from parents shall be accessible by children , placements :: [Placement] -- ^ earlier placements will be overlaid on top of later placements in the YAML file + , waypoints :: [Waypoint] } deriving (Eq, Show) -newtype MergedStructure c = MergedStructure [[c]] +data MergedStructure c = MergedStructure [[c]] [Originated Waypoint] +-- | Orientation tranformations are applied before translation. data Orientation = Orientation { up :: AbsoluteDir + -- ^ e.g. For "East", rotates 270 degrees. , flipped :: Bool -- ^ vertical flip, applied before rotation } @@ -77,13 +91,18 @@ overlaySingleStructure :: MergedStructure (Maybe a) overlaySingleStructure inheritedStrucDefs - (Placement _ (Location colOffset rowOffset) orientation, struc) - (MergedStructure inputArea) = - MergedStructure $ zipWithPad mergeSingleRow inputArea paddedOverlayRows + (p@(Placement _ loc@(Location colOffset rowOffset) orientation), struc) + (MergedStructure inputArea inputWaypoints) = + MergedStructure mergedArea mergedWaypoints where + mergedArea = zipWithPad mergeSingleRow inputArea paddedOverlayRows + + mergedWaypoints = inputWaypoints ++ map (fmap $ offsetWaypoint (coerce loc) . reorientWaypoint orientation (getAreaDimensions overlayArea)) overlayWaypoints + zipWithPad f a b = zipWith f a $ b <> repeat Nothing - MergedStructure overlayArea = mergeStructures inheritedStrucDefs struc - affineTransformedOverlay = getTransform orientation overlayArea + + MergedStructure overlayArea overlayWaypoints = mergeStructures inheritedStrucDefs (Just p) struc + affineTransformedOverlay = applyOrientationTransform orientation overlayArea mergeSingleRow inputRow maybeOverlayRow = zipWithPad (flip (<|>)) inputRow paddedSingleOverlayRow @@ -99,12 +118,18 @@ overlaySingleStructure then (replicate integralOffset Nothing <>) else drop $ abs integralOffset --- | Overlays all of the "child placements", such that the --- earlier children supersede the later ones (due to use of "foldr" instead of "foldl"). -mergeStructures :: M.Map StructureName (PStructure (Maybe a)) -> PStructure (Maybe a) -> MergedStructure (Maybe a) -mergeStructures inheritedStrucDefs (Structure origArea subStructures subPlacements) = - foldr (overlaySingleStructure structureMap) (MergedStructure origArea) overlays +-- | Overlays all of the "child placements", such that the children encountered earlier +-- in the YAML file supersede the later ones (due to use of "foldr" instead of "foldl"). +mergeStructures :: + M.Map StructureName (PStructure (Maybe a)) -> + Maybe Placement -> + PStructure (Maybe a) -> + MergedStructure (Maybe a) +mergeStructures inheritedStrucDefs parentPlacement (Structure origArea subStructures subPlacements subWaypoints) = + foldr (overlaySingleStructure structureMap) (MergedStructure origArea originatedWaypoints) overlays where + originatedWaypoints = map (Originated parentPlacement) subWaypoints + -- deeper definitions override the outer (toplevel) ones structureMap = M.union (M.fromList $ map (name &&& structure) subStructures) inheritedStrucDefs overlays = mapMaybe g subPlacements @@ -116,13 +141,28 @@ instance FromJSONE (EntityMap, RobotMap) (PStructure (Maybe (PCell Entity))) whe pal <- v ..:? "palette" ..!= WorldPalette mempty structureDefs <- v ..:? "structures" ..!= [] placementDefs <- liftE $ v .:? "placements" .!= [] + waypointDefs <- liftE $ v .:? "waypoints" .!= [] maybeMaskChar <- liftE $ v .:? "mask" - maskedArea <- liftE $ (v .:? "map" .!= "") >>= paintMap maybeMaskChar pal - return $ Structure maskedArea structureDefs placementDefs + (maskedArea, mapWaypoints) <- liftE $ (v .:? "map" .!= "") >>= paintMap maybeMaskChar pal + return $ Structure maskedArea structureDefs placementDefs $ waypointDefs <> mapWaypoints + +reorientWaypoint :: Orientation -> AreaDimensions -> Waypoint -> Waypoint +reorientWaypoint (Orientation upDir shouldFlip) (AreaDimensions width height) = + modifyLocation $ rotational . flipping + where + transposeLoc (Location x y) = Location (-y) (-x) + flipV (Location x y) = Location x $ -(height - 1) - y + flipH (Location x y) = Location (width - 1 - x) y + flipping = if shouldFlip then flipV else id + rotational = case upDir of + DNorth -> id + DSouth -> flipH . flipV + DEast -> transposeLoc . flipV + DWest -> transposeLoc . flipH -- | affine transformation -getTransform :: Orientation -> ([[a]] -> [[a]]) -getTransform (Orientation upDir shouldFlip) = +applyOrientationTransform :: Orientation -> [[a]] -> [[a]] +applyOrientationTransform (Orientation upDir shouldFlip) = rotational . flipping where flipV = reverse @@ -138,7 +178,7 @@ data Placement = Placement , offset :: Location , orient :: Orientation } - deriving (Eq, Show) + deriving (Show, Eq) instance FromJSON Placement where parseJSON = withObject "structure placement" $ \v -> do @@ -151,8 +191,21 @@ instance FromJSON Placement where -- string into a nested list of 'Cell' values by looking up each -- character in the palette, failing if any character in the raw map -- is not contained in the palette. -paintMap :: MonadFail m => Maybe Char -> WorldPalette e -> Text -> m [[Maybe (PCell e)]] -paintMap maskChar pal = readMap toCell +paintMap :: + MonadFail m => + Maybe Char -> + WorldPalette e -> + Text -> + m ([[Maybe (PCell e)]], [Waypoint]) +paintMap maskChar pal a = do + nestedLists <- readMap toCell a + let cells = map (map $ fmap standardCell) nestedLists + f i j maybeAugmentedCell = do + wpcfg <- waypointCfg =<< maybeAugmentedCell + return . Waypoint wpcfg . Location j $ negate i + wps = concat $ zipWith (\i -> catMaybes . zipWith (f i) [0 ..]) [0 ..] nestedLists + + return (cells, wps) where toCell c = if Just c == maskChar diff --git a/src/Swarm/Game/Scenario/Waypoint.hs b/src/Swarm/Game/Scenario/Waypoint.hs new file mode 100644 index 0000000000..0088c2a9bd --- /dev/null +++ b/src/Swarm/Game/Scenario/Waypoint.hs @@ -0,0 +1,61 @@ +{-# LANGUAGE OverloadedStrings #-} + +-- | +-- SPDX-License-Identifier: BSD-3-Clause +module Swarm.Game.Scenario.Waypoint where + +import Data.Int (Int32) +import Data.Text qualified as T +import Data.Yaml as Y +import GHC.Generics (Generic) +import Linear (V2 (..)) +import Swarm.Game.Location + +newtype WaypointName = WaypointName T.Text + deriving (Show, Eq, Ord, Generic, FromJSON) + +-- | Metadata about a waypoint +data WaypointConfig = WaypointConfig + { wpName :: WaypointName + , wpUnique :: Bool + -- ^ Enforce global uniqueness of this waypoint + } + deriving (Show, Eq) + +parseWaypointConfig :: Object -> Parser WaypointConfig +parseWaypointConfig v = + WaypointConfig + <$> v .: "name" + <*> v .:? "unique" .!= False + +instance FromJSON WaypointConfig where + parseJSON = withObject "Waypoint Config" parseWaypointConfig + +-- | +-- A parent world shouldn't have to know the exact layout of a subworld +-- to specify where exactly a portal will deliver a robot to within the subworld. +-- Therefore, we define named waypoints in the subworld and the parent world +-- must reference them by name, rather than by coordinate. +data Waypoint = Waypoint + { wpConfig :: WaypointConfig + , wpLoc :: Location + } + deriving (Show, Eq) + +instance FromJSON Waypoint where + parseJSON = withObject "Waypoint" $ \v -> + Waypoint + <$> parseWaypointConfig v + <*> v .: "loc" + +modifyLocation :: + (Location -> Location) -> + Waypoint -> + Waypoint +modifyLocation f (Waypoint cfg originalLoc) = Waypoint cfg $ f originalLoc + +offsetWaypoint :: + V2 Int32 -> + Waypoint -> + Waypoint +offsetWaypoint locOffset = modifyLocation (.+^ locOffset) diff --git a/src/Swarm/Game/Scenario/WorldDescription.hs b/src/Swarm/Game/Scenario/WorldDescription.hs index f5eeab9032..1f1d734c67 100644 --- a/src/Swarm/Game/Scenario/WorldDescription.hs +++ b/src/Swarm/Game/Scenario/WorldDescription.hs @@ -5,21 +5,40 @@ -- SPDX-License-Identifier: BSD-3-Clause module Swarm.Game.Scenario.WorldDescription where -import Data.Maybe (catMaybes) +import Control.Monad (forM, forM_, unless) +import Data.Coerce +import Data.List (intercalate) +import Data.List.NonEmpty (NonEmpty ((:|))) +import Data.List.NonEmpty qualified as NE +import Data.Map qualified as M +import Data.Maybe (catMaybes, listToMaybe) +import Data.Text qualified as T import Data.Yaml as Y import Swarm.Game.Entity import Swarm.Game.Location import Swarm.Game.Scenario.Cell import Swarm.Game.Scenario.EntityFacade +import Swarm.Game.Scenario.Portal import Swarm.Game.Scenario.RobotLookup import Swarm.Game.Scenario.Structure qualified as Structure +import Swarm.Game.Scenario.Waypoint import Swarm.Game.Scenario.WorldPalette +import Swarm.Util (binTuples, quote) import Swarm.Util.Yaml ------------------------------------------------------------ -- World description ------------------------------------------------------------ +data Landmarks = Landmarks + { waypoints :: M.Map WaypointName (NonEmpty Location) + -- ^ Note that waypoints defined at the "root" level are still relative to + -- the top-left corner of the map rectangle; they are not in absolute world + -- coordinates (as with applying the "ul" offset). + , portals :: M.Map Location Location + } + deriving (Eq, Show) + -- | A description of a world parsed from a YAML file. -- This type is parameterized to accommodate Cells that -- utilize a less stateful Entity type. @@ -30,28 +49,87 @@ data PWorldDescription e = WorldDescription , palette :: WorldPalette e , ul :: Location , area :: [[PCell e]] + , landmarks :: Landmarks } deriving (Eq, Show) type WorldDescription = PWorldDescription Entity +failUponDuplication :: + (MonadFail m, Show a, Show b) => + String -> + M.Map a (NonEmpty b) -> + m () +failUponDuplication message binnedMap = + forM_ (listToMaybe $ M.toList duplicated) $ \(pIn, pOuts) -> + fail $ + unwords + [ "Waypoint" + , show pIn + , message + , intercalate ", " $ map show $ NE.toList pOuts + ] + where + duplicated = M.filter ((> 1) . NE.length) binnedMap + instance FromJSONE (EntityMap, RobotMap) WorldDescription where parseJSONE = withObjectE "world description" $ \v -> do pal <- v ..:? "palette" ..!= WorldPalette mempty structureDefs <- v ..:? "structures" ..!= [] + waypointDefs <- liftE $ v .:? "waypoints" .!= [] + portalDefs <- liftE $ v .:? "portals" .!= [] placementDefs <- liftE $ v .:? "placements" .!= [] - initialArea <- liftE ((v .:? "map" .!= "") >>= Structure.paintMap Nothing pal) + (initialArea, mapWaypoints) <- liftE ((v .:? "map" .!= "") >>= Structure.paintMap Nothing pal) + + upperLeft <- liftE (v .:? "upperleft" .!= origin) + + let struc = Structure.Structure initialArea structureDefs placementDefs $ waypointDefs <> mapWaypoints + Structure.MergedStructure mergedArea unmergedWaypoints = Structure.mergeStructures mempty Nothing struc + + extractLoc (Structure.Originated _ (Waypoint _ loc)) = loc + correctedWaypoints = + binTuples $ + map + (\x@(Structure.Originated _ (Waypoint (WaypointConfig n _) _loc)) -> (n, fmap (offsetWaypoint (coerce upperLeft)) x)) + unmergedWaypoints + bareWaypoints = M.map (NE.map extractLoc) correctedWaypoints + + waypointsWithUniqueFlag = M.filter (any $ wpUnique . wpConfig . Structure.value) correctedWaypoints + + failUponDuplication "is required to be unique, but is duplicated in:" waypointsWithUniqueFlag + + -- TODO(#144) Currently ignores subworld references + nestedPortalPairs <- forM portalDefs $ \(Portal entranceName (PortalExit exitName@(WaypointName rawExitName) _)) -> do + let getLocs wpWrapper@(WaypointName rawName) = case M.lookup wpWrapper correctedWaypoints of + Nothing -> fail $ T.unpack $ T.unwords ["No waypoint named", quote rawName] + Just xs -> return xs + + -- Portals can have multiple entrances but only a single exit. + -- That is, the pairings of entries to exits must form a proper mathematical "function". + -- Multiple occurrences of entrance waypoints of a given name will replicate portal entrances. + entranceLocs <- getLocs entranceName + firstExitLoc :| otherExits <- getLocs exitName + unless (null otherExits) + . fail + . T.unpack + $ T.unwords ["Ambiguous exit waypoints named", quote rawExitName, "for portal"] + return $ map ((,extractLoc firstExitLoc) . extractLoc) $ NE.toList entranceLocs + + let reconciledPortalPairs = concat nestedPortalPairs - let struc = Structure.Structure initialArea structureDefs placementDefs - Structure.MergedStructure mergedArea = Structure.mergeStructures mempty struc + -- Aside from the enforcement of single-exit per portal, we apply another layer of + -- enforcement to ensure that no two portals share the same entrance location + failUponDuplication "has overlapping portal entrances exiting to" $ + binTuples reconciledPortalPairs WorldDescription <$> v ..:? "default" <*> liftE (v .:? "offset" .!= False) <*> liftE (v .:? "scrollable" .!= True) <*> pure pal - <*> liftE (v .:? "upperleft" .!= origin) + <*> pure upperLeft <*> pure (map catMaybes mergedArea) -- Root-level map has no transparent cells. + <*> pure (Landmarks bareWaypoints $ M.fromList reconciledPortalPairs) ------------------------------------------------------------ -- World editor diff --git a/src/Swarm/Game/Scenario/WorldPalette.hs b/src/Swarm/Game/Scenario/WorldPalette.hs index aa183c5050..f4360ab62b 100644 --- a/src/Swarm/Game/Scenario/WorldPalette.hs +++ b/src/Swarm/Game/Scenario/WorldPalette.hs @@ -22,7 +22,7 @@ import Swarm.Util.Yaml -- | A world palette maps characters to 'Cell' values. newtype WorldPalette e = WorldPalette - {unPalette :: KeyMap (PCell e)} + {unPalette :: KeyMap (AugmentedCell e)} deriving (Eq, Show) instance FromJSONE (EntityMap, RobotMap) (WorldPalette Entity) where @@ -100,7 +100,7 @@ prepForJson (WorldPalette suggestedPalette) cellGrid = where preassignments :: [(Char, TerrainWith EntityFacade)] preassignments = - map (first T.head . fmap cellToTerrainPair) $ + map (first T.head . fmap (cellToTerrainPair . standardCell)) $ M.toList $ KM.toMapText suggestedPalette diff --git a/src/Swarm/Game/State.hs b/src/Swarm/Game/State.hs index ea7078d41e..29c2daf3b2 100644 --- a/src/Swarm/Game/State.hs +++ b/src/Swarm/Game/State.hs @@ -60,6 +60,7 @@ module Swarm.Game.State ( recipesReq, currentScenarioPath, knownEntities, + worldLandmarks, world, worldScrollable, viewCenterRule, @@ -169,6 +170,7 @@ import Swarm.Game.Recipe ( import Swarm.Game.Robot import Swarm.Game.Scenario.Objective import Swarm.Game.Scenario.Status +import Swarm.Game.Scenario.WorldDescription (Landmarks (..)) import Swarm.Game.ScenarioInfo import Swarm.Game.Terrain (TerrainType (..)) import Swarm.Game.World (Coords (..), WorldFun (..), locToCoords, worldFunFromArray) @@ -398,6 +400,7 @@ data GameState = GameState , _recipesReq :: IntMap [Recipe Entity] , _currentScenarioPath :: Maybe FilePath , _knownEntities :: [Text] + , _worldLandmarks :: Landmarks , _world :: W.World Int Entity , _worldScrollable :: Bool , _viewCenterRule :: ViewCenterRule @@ -559,6 +562,10 @@ currentScenarioPath :: Lens' GameState (Maybe FilePath) -- robots know what they are without having to scan them. knownEntities :: Lens' GameState [Text] +-- | Dictionary of named locations and an +-- "Edge list" (Graph) mapping portal entrances to exits +worldLandmarks :: Lens' GameState Landmarks + -- | The current state of the world (terrain and entities only; robots -- are stored in the 'robotMap'). Int is used instead of -- TerrainType because we need to be able to store terrain values in @@ -995,6 +1002,7 @@ initGameState gsc = , _recipesReq = reqRecipeMap (initRecipes gsc) , _currentScenarioPath = Nothing , _knownEntities = [] + , _worldLandmarks = Landmarks mempty mempty , _world = W.emptyWorld (fromEnum StoneT) , _worldScrollable = True , _viewCenterRule = VCRobot 0 @@ -1051,6 +1059,7 @@ scenarioToGameState scenario (LaunchParams (Identity userSeed) (Identity toRun)) & recipesIn %~ addRecipesWith inRecipeMap & recipesReq %~ addRecipesWith reqRecipeMap & knownEntities .~ scenario ^. scenarioKnown + & worldLandmarks .~ landmarks (scenario ^. scenarioWorld) & world .~ theWorld theSeed & worldScrollable .~ scenario ^. scenarioWorld . to scrollable & viewCenterRule .~ VCRobot baseID diff --git a/src/Swarm/Game/Step.hs b/src/Swarm/Game/Step.hs index 241c59a296..170284f738 100644 --- a/src/Swarm/Game/Step.hs +++ b/src/Swarm/Game/Step.hs @@ -47,6 +47,7 @@ import Data.IntMap qualified as IM import Data.IntSet qualified as IS import Data.List (find, sortOn) import Data.List qualified as L +import Data.List.NonEmpty qualified as NE import Data.Map qualified as M import Data.Maybe (catMaybes, fromMaybe, isJust, isNothing, listToMaybe, mapMaybe) import Data.Ord (Down (Down)) @@ -74,6 +75,8 @@ import Swarm.Game.ResourceLoading (getDataFileNameSafe) import Swarm.Game.Robot import Swarm.Game.Scenario.Objective qualified as OB import Swarm.Game.Scenario.Objective.WinCheck qualified as WC +import Swarm.Game.Scenario.Waypoint (WaypointName (..)) +import Swarm.Game.Scenario.WorldDescription (Landmarks (..)) import Swarm.Game.State import Swarm.Game.Value import Swarm.Game.World qualified as W @@ -1398,6 +1401,13 @@ execConst c vs s k = do Whereami -> do loc <- use robotLocation return $ Out (asValue loc) s k + Waypoint -> case vs of + [VText name, VInt idx] -> do + lm <- use worldLandmarks + case M.lookup (WaypointName name) (waypoints lm) of + Nothing -> throwError $ CmdFailed Waypoint (T.unwords ["No waypoint named", name]) Nothing + Just wps -> return $ Out (asValue (NE.length wps, indexWrapNonEmpty wps $ fromIntegral idx)) s k + _ -> badConst Detect -> case vs of [VText name, VRect x1 y1 x2 y2] -> do loc <- use robotLocation @@ -2669,6 +2679,7 @@ provisionChild childID toEquip toGive = do -- 'robotsByLocation' map, so we can always look up robots by -- location. This should be the /only/ way to update the location -- of a robot. +-- Also implements teleportation by portals. updateRobotLocation :: (HasRobotStepState sig m) => Location -> @@ -2677,12 +2688,17 @@ updateRobotLocation :: updateRobotLocation oldLoc newLoc | oldLoc == newLoc = return () | otherwise = do + newlocWithPortal <- applyPortal newLoc rid <- use robotID robotsByLocation . at oldLoc %= deleteOne rid - robotsByLocation . at newLoc . non Empty %= IS.insert rid - modify (unsafeSetRobotLocation newLoc) + robotsByLocation . at newlocWithPortal . non Empty %= IS.insert rid + modify (unsafeSetRobotLocation newlocWithPortal) flagRedraw where + applyPortal loc = do + lms <- use worldLandmarks + return $ M.findWithDefault loc loc $ portals lms + -- Make sure empty sets don't hang around in the -- robotsByLocation map. We don't want a key with an -- empty set at every location any robot has ever diff --git a/src/Swarm/Game/Value.hs b/src/Swarm/Game/Value.hs index 4bc0cb75d0..954f3c085b 100644 --- a/src/Swarm/Game/Value.hs +++ b/src/Swarm/Game/Value.hs @@ -35,8 +35,17 @@ class Valuable a where instance Valuable Int32 where asValue = VInt . fromIntegral +instance Valuable Int where + asValue = VInt . fromIntegral + instance (Valuable a) => Valuable (V2 a) where - asValue (V2 x y) = VPair (asValue x) (asValue y) + asValue (V2 x y) = asValue (x, y) + +instance (Valuable a, Valuable b) => Valuable (a, b) where + asValue (x, y) = VPair (asValue x) (asValue y) + +instance Valuable Location where + asValue (Location x y) = VPair (asValue x) (asValue y) instance Valuable Entity where asValue = VText . view entityName @@ -44,9 +53,6 @@ instance Valuable Entity where instance Valuable Robot where asValue = VRobot . view robotID -instance Valuable Location where - asValue (Location x y) = VPair (VInt (fromIntegral x)) (VInt (fromIntegral y)) - instance (Valuable a) => Valuable (Maybe a) where asValue Nothing = VInj False VUnit asValue (Just x) = VInj True $ asValue x diff --git a/src/Swarm/Language/Capability.hs b/src/Swarm/Language/Capability.hs index 61498e0bde..432727e3e3 100644 --- a/src/Swarm/Language/Capability.hs +++ b/src/Swarm/Language/Capability.hs @@ -246,6 +246,7 @@ constCaps = \case Wait -> Just CTimerel Scout -> Just CRecondir Whereami -> Just CSenseloc + Waypoint -> Just CGod Detect -> Just CDetectloc Resonate -> Just CDetectcount Density -> Just CDetectcount diff --git a/src/Swarm/Language/Syntax.hs b/src/Swarm/Language/Syntax.hs index fc74f6715f..56a70c848c 100644 --- a/src/Swarm/Language/Syntax.hs +++ b/src/Swarm/Language/Syntax.hs @@ -269,6 +269,8 @@ data Const Scout | -- | Get the current x, y coordinates Whereami + | -- | Get the x, y coordinates of a named waypoint, by index + Waypoint | -- | Locate the closest instance of a given entity within the rectangle -- specified by opposite corners, relative to the current location. Detect @@ -664,6 +666,11 @@ constInfo c = case c of , T.unwords ["Has a max range of", T.pack $ show maxScoutRange, "units."] ] Whereami -> command 0 Intangible "Get the current x and y coordinates." + Waypoint -> + command 2 Intangible . doc "Get the x, y coordinates of a named waypoint, by index" $ + [ "Since waypoint names can have plural multiplicity, returns a tuple of (count, (x, y))" + , "A robot can then iterate over all of the waypoints of that name by specifying an index less than the count." + ] Detect -> command 2 Intangible . doc "Detect an entity within a rectangle." $ ["Locate the closest instance of a given entity within the rectangle specified by opposite corners, relative to the current location."] diff --git a/src/Swarm/Language/Typecheck.hs b/src/Swarm/Language/Typecheck.hs index 05175b8ec2..0e38ec4768 100644 --- a/src/Swarm/Language/Typecheck.hs +++ b/src/Swarm/Language/Typecheck.hs @@ -731,6 +731,7 @@ inferConst c = case c of Time -> [tyQ| cmd int |] Scout -> [tyQ| dir -> cmd bool |] Whereami -> [tyQ| cmd (int * int) |] + Waypoint -> [tyQ| text -> int -> cmd (int * (int * int)) |] Detect -> [tyQ| text -> ((int * int) * (int * int)) -> cmd (unit + (int * int)) |] Resonate -> [tyQ| text -> ((int * int) * (int * int)) -> cmd int |] Density -> [tyQ| ((int * int) * (int * int)) -> cmd int |] diff --git a/src/Swarm/TUI/Editor/Palette.hs b/src/Swarm/TUI/Editor/Palette.hs index 875b3ceed7..9b6a8c308f 100644 --- a/src/Swarm/TUI/Editor/Palette.hs +++ b/src/Swarm/TUI/Editor/Palette.hs @@ -23,6 +23,7 @@ import Swarm.Game.Location import Swarm.Game.Scenario import Swarm.Game.Scenario.Cell import Swarm.Game.Scenario.EntityFacade +import Swarm.Game.Scenario.WorldDescription (Landmarks (..)) import Swarm.Game.Scenario.WorldPalette import Swarm.Game.Terrain (TerrainType (BlankT), getTerrainDefaultPaletteChar) import Swarm.TUI.Editor.Area (AreaDimensions (..), getAreaDimensions) @@ -30,9 +31,10 @@ import Swarm.TUI.Editor.Json (SkeletonScenario (SkeletonScenario)) import Swarm.Util (binTuples, histogram) import Swarm.Util qualified as U -makeSuggestedPalette :: Maybe Scenario -> [[CellPaintDisplay]] -> KM.KeyMap CellPaintDisplay +makeSuggestedPalette :: Maybe Scenario -> [[CellPaintDisplay]] -> KM.KeyMap (AugmentedCell EntityFacade) makeSuggestedPalette maybeOriginalScenario cellGrid = KM.fromMapText + . M.map (AugmentedCell Nothing) . M.fromList . M.elems -- NOTE: the left-most maps take precedence! @@ -83,7 +85,7 @@ makeSuggestedPalette maybeOriginalScenario cellGrid = originalPalette :: KM.KeyMap CellPaintDisplay originalPalette = - KM.map toCellPaintDisplay $ + KM.map (toCellPaintDisplay . standardCell) $ maybe mempty (unPalette . palette . (^. scenarioWorld)) maybeOriginalScenario pairsWithDisplays :: Map (TerrainWith EntityName) (T.Text, CellPaintDisplay) @@ -125,6 +127,7 @@ constructScenario maybeOriginalScenario cellGrid = , palette = WorldPalette suggestedPalette , ul = upperLeftCoord , area = cellGrid + , landmarks = Landmarks mempty mempty } suggestedPalette = makeSuggestedPalette maybeOriginalScenario cellGrid diff --git a/src/Swarm/Util.hs b/src/Swarm/Util.hs index 31969a67c2..5ff9f300a2 100644 --- a/src/Swarm/Util.hs +++ b/src/Swarm/Util.hs @@ -15,6 +15,7 @@ module Swarm.Util ( maximum0, cycleEnum, listEnums, + indexWrapNonEmpty, uniq, binTuples, histogram, @@ -137,6 +138,13 @@ cycleEnum e listEnums :: (Enum e, Bounded e) => [e] listEnums = [minBound .. maxBound] +-- | Guaranteed to yield an element of the list +indexWrapNonEmpty :: NonEmpty a -> Int -> a +indexWrapNonEmpty list idx = + NE.toList list !! wrappedIdx + where + wrappedIdx = idx `mod` NE.length list + -- | Drop repeated elements that are adjacent to each other. -- -- >>> uniq [] diff --git a/src/Swarm/Util/Yaml.hs b/src/Swarm/Util/Yaml.hs index 16de73c315..90c934d459 100644 --- a/src/Swarm/Util/Yaml.hs +++ b/src/Swarm/Util/Yaml.hs @@ -24,6 +24,7 @@ module Swarm.Util.Yaml ( withArrayE, ) where +import Control.Applicative (Alternative) import Control.Monad.Reader import Data.Aeson.Key (fromText) import Data.Aeson.Types (explicitParseField, explicitParseFieldMaybe) @@ -42,13 +43,26 @@ import Swarm.Util (failT, showT) -- value of type @e@. newtype With e f a = E {runE :: e -> f a} deriving (Functor) - deriving (Applicative, Monad, MonadFail) via (ReaderT e f) + deriving (Applicative, Monad, MonadFail, Alternative) via (ReaderT e f) -- | A 'ParserE' is a YAML 'Parser' that can also depend on knowing an -- value of type @e@. The @E@ used to stand for @EntityMap@, but now -- that it is generalized, it stands for Environment. type ParserE e = With e Parser +-- instance Alternative (With a Parser) where +-- empty = fail "empty" +-- {-# INLINE empty #-} +-- (<|>) = mplus +-- {-# INLINE (<|>) #-} + +-- instance MonadPlus (With a Parser) where +-- mzero = fail "mzero" +-- {-# INLINE mzero #-} +-- mplus a b = pure $ \path kf ks -> let kf' _ _ = runParser b path kf ks +-- in runParser a path kf' ks +-- {-# INLINE mplus #-} + -- | Lift a computation that does not care about the environment -- value. liftE :: Functor f => f a -> With e f a diff --git a/swarm.cabal b/swarm.cabal index 6ea852efcd..36616e27cb 100644 --- a/swarm.cabal +++ b/swarm.cabal @@ -113,6 +113,7 @@ library Swarm.Game.Scenario.Objective.Logic Swarm.Game.Scenario.Objective.Validation Swarm.Game.Scenario.Objective.WinCheck + Swarm.Game.Scenario.Portal Swarm.Game.Scenario.RobotLookup Swarm.Game.Scenario.Scoring.Best Swarm.Game.Scenario.Scoring.CodeSize @@ -121,6 +122,7 @@ library Swarm.Game.Scenario.Status Swarm.Game.Scenario.Structure Swarm.Game.Scenario.Style + Swarm.Game.Scenario.Waypoint Swarm.Game.Scenario.WorldDescription Swarm.Game.Scenario.WorldPalette Swarm.Game.ScenarioInfo diff --git a/test/integration/Main.hs b/test/integration/Main.hs index a5d659d336..933677b1b4 100644 --- a/test/integration/Main.hs +++ b/test/integration/Main.hs @@ -296,6 +296,7 @@ testScenarioSolution _ci _em = , testSolution Default "Testing/1234-push-command" , testSolution Default "Testing/1256-halt-command" , testSolution Default "Testing/1295-density-command" + , testSolution Default "Testing/1356-portals/portals-flip-and-rotate.yaml" ] ] where