packages feed

swarm-0.7.0.0: src/swarm-topography/Swarm/Game/Scenario/Topography/Structure.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}

-- |
-- SPDX-License-Identifier: BSD-3-Clause
--
-- Definitions of "structures" for use within a map
-- as well as logic for combining them.
module Swarm.Game.Scenario.Topography.Structure where

import Control.Monad (forM_, unless)
import Data.List (intercalate)
import Data.List.NonEmpty qualified as NE
import Data.Map qualified as M
import Data.Maybe (catMaybes, fromMaybe)
import Data.Set qualified as Set
import Data.Text qualified as T
import Data.Yaml as Y
import Swarm.Game.Location
import Swarm.Game.Scenario.Topography.Grid
import Swarm.Game.Scenario.Topography.Navigation.Waypoint
import Swarm.Game.Scenario.Topography.Placement
import Swarm.Game.Scenario.Topography.ProtoCell
import Swarm.Game.Scenario.Topography.Structure.Named
import Swarm.Game.Scenario.Topography.Structure.Overlay
import Swarm.Game.Scenario.Topography.Structure.Recognition.Static
import Swarm.Game.World.Coords
import Swarm.Util (failT, showT)
import Swarm.Util.Yaml

type NamedStructure c = NamedArea (PStructure c)

data PStructure c = Structure
  { area :: PositionedGrid c
  , structures :: [NamedStructure c]
  -- ^ 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)

data Placed c = Placed Placement (NamedStructure c)
  deriving (Show)

data MergedStructure c = MergedStructure (PositionedGrid c) [LocatedStructure] [Originated Waypoint]

instance (FromJSONE e a) => FromJSONE e (NamedStructure (Maybe a)) where
  parseJSONE = withObjectE "named structure" $ \v -> do
    structure <- v ..: "structure"
    liftE $ do
      name <- v .: "name"
      recognize <- v .:? "recognize" .!= mempty
      description <- v .:? "description"
      return $ NamedArea {..}

instance FromJSON (Grid Char) where
  parseJSON = withText "area" $ \t -> do
    let textLines = map T.unpack $ T.lines t
        g = mkGrid textLines
    case NE.nonEmpty textLines of
      Nothing -> return EmptyGrid
      Just nonemptyRows -> do
        let firstRowLength = length $ NE.head nonemptyRows
        unless (all ((== firstRowLength) . length) $ NE.tail nonemptyRows) $
          fail "Grid is not rectangular!"
        return g

parseStructure ::
  StructurePalette c ->
  [NamedStructure (Maybe c)] ->
  Object ->
  Parser (PStructure (Maybe c))
parseStructure pal structures v = do
  explicitPlacements <- v .:? "placements" .!= []
  waypointDefs <- v .:? "waypoints" .!= []
  maybeMaskChar <- v .:? "mask"
  rawGrid <- v .:? "map" .!= EmptyGrid
  (maskedArea, mapWaypoints, palettePlacements) <- paintMap maybeMaskChar pal rawGrid
  let area = PositionedGrid origin maskedArea
      waypoints = waypointDefs <> mapWaypoints
      placements = explicitPlacements <> palettePlacements
  return Structure {..}

instance (FromJSONE e a) => FromJSONE e (PStructure (Maybe a)) where
  parseJSONE = withObjectE "structure definition" $ \v -> do
    pal <- v ..:? "palette" ..!= StructurePalette mempty
    structures <- v ..:? "structures" ..!= []
    liftE $ parseStructure pal structures v

-- | \"Paint\" a world map using a 'WorldPalette', turning it from a raw
--   string into a nested list of 'PCell' 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 ->
  StructurePalette c ->
  Grid Char ->
  m (Grid (Maybe c), [Waypoint], [Placement])
paintMap maskChar pal g = do
  nestedLists <- mapM toCell g
  forM_ maskChar $ \c ->
    unless (Set.notMember c paletteKeys) $
      fail $
        unwords
          [ "Mask character"
          , ['"', c, '"']
          , "overlaps palette entry"
          ]

  unless (Set.null unusedPaletteChars) $
    fail $
      unwords
        [ "Unused characters in palette:"
        , intercalate ", " $ map pure $ Set.toList unusedPaletteChars
        ]

  let cells = fmap standardCell <$> nestedLists
      wps = catMaybes $ mapWithCoords getWp nestedLists

  let extraPlacements =
        catMaybes $ mapWithCoords getStructureMarker nestedLists

  return (cells, wps, extraPlacements)
 where
  getStructureMarker coords maybeAugmentedCell = do
    StructureMarker sName orientation <- structureMarker =<< maybeAugmentedCell
    return
      . Placement sName
      . Pose (coordsToLoc coords)
      $ fromMaybe defaultOrientation orientation

  getWp coords maybeAugmentedCell = do
    wpCfg <- waypointCfg =<< maybeAugmentedCell
    return . Waypoint wpCfg . coordsToLoc $ coords

  usedChars = Set.fromList $ allMembers g
  paletteKeys = M.keysSet $ unPalette pal
  unusedPaletteChars = Set.difference paletteKeys usedChars

  toCell c =
    if Just c == maskChar
      then return Nothing
      else case M.lookup c (unPalette pal) of
        Nothing -> failT ["Char not in world palette:", showT c]
        Just cell -> return $ Just cell