packages feed

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

-- |
-- SPDX-License-Identifier: BSD-3-Clause
--
-- Precomputation for structure recognizer.
--
-- = Search process overview
--
-- 2D structures may be defined at the
-- <https://github.com/swarm-game/swarm/blob/main/data/scenarios/_doc-fragments/SCHEMA.md#top-level toplevel of a scenario file>.
-- Upon scenario load, all of the predefined structures that are marked
-- as @"recognize"@ are compiled into searcher state machines.
--
-- When an entity is placed on any cell in the world, the
-- 'Swarm.Game.Scenario.Topography.Structure.Recognition.Tracking.entityModified'
-- function is called, which looks up a customized searcher based
-- on the type of placed entity.
--
-- The first searching stage looks for any member row of all participating
-- structure definitions that contains the placed entity.
-- If we observe a row in the world that happens to occur in a structure, we use both
-- the horizontal found offset and the index of the row within this structure to compute
-- the expected world location of the candidate structure.
-- Then we perform a full scan of that candidate structure against the world to verify
-- the match.
--
-- Upon locating a complete structure, it is added to a registry
-- (see 'Swarm.Game.Scenario.Topography.Structure.Recognition.Registry.FoundRegistry'), which
-- supports lookups by either name or by location (using two different
-- maps maintained in parallel). The map by location is used to remove
-- a structure from the registry if a member entity is changed.
module Swarm.Game.Scenario.Topography.Structure.Recognition.Precompute (
  -- * Main external interface
  mkAutomatons,

  -- * Types
  GenericEntLocator,

  -- * Helper functions
  populateStaticFoundStructures,
  lookupStaticPlacements,
  ensureStructureIntact,
) where

import Control.Arrow ((&&&))
import Control.Lens ((^.))
import Control.Monad (forM_, unless)
import Control.Monad.Trans.Class (lift)
import Control.Monad.Trans.Except (except, runExceptT)
import Data.Either.Combinators (leftToMaybe)
import Data.Hashable (Hashable)
import Data.List.NonEmpty qualified as NE
import Data.Map qualified as M
import Data.Maybe (catMaybes, mapMaybe)
import Data.Set qualified as Set
import Data.Tuple (swap)
import Data.Tuple.Extra (dupe)
import Swarm.Game.Location (Location, asVector)
import Swarm.Game.Scenario.Topography.Area (getNEGridDimensions, rectWidth)
import Swarm.Game.Scenario.Topography.Grid
import Swarm.Game.Scenario.Topography.Placement (Orientation (..), applyOrientationTransformNE)
import Swarm.Game.Scenario.Topography.Structure.Named
import Swarm.Game.Scenario.Topography.Structure.Recognition.Prep (
  mkEntityLookup,
 )
import Swarm.Game.Scenario.Topography.Structure.Recognition.Registry (
  FoundRegistry,
  foundByLocation,
  populateStaticFoundStructures,
 )
import Swarm.Game.Scenario.Topography.Structure.Recognition.Static
import Swarm.Game.Scenario.Topography.Structure.Recognition.Symmetry
import Swarm.Game.Scenario.Topography.Structure.Recognition.Type
import Swarm.Game.Universe (Cosmic (..), offsetBy, planar)
import Swarm.Game.World.Coords (coordsToLoc)
import Swarm.Language.Syntax.Direction (AbsoluteDir)
import Swarm.Util (histogram)

-- | Interface that provides monadic access to
-- querying entities at locations.
-- The provider may be a 'State' monad or just
-- a 'Reader'.
--
-- 's' is the state variable, 'a' is the return type.
type GenericEntLocator s a = Cosmic Location -> s (AtomicKeySymbol a)

-- | Create Aho-Corasick matchers that will recognize all of the
-- provided structure definitions
mkAutomatons ::
  (Ord a, Hashable a) =>
  (b -> NonEmptyGrid (AtomicKeySymbol a)) ->
  [NamedArea b] ->
  Either RedundantOrientations (RecognizerAutomatons b a)
mkAutomatons extractor rawGrids = do
  onlyNonempties <- mapM checkSymmetry extractedItems
  let rotatedGrids = concatMap (extractGrids . grid) onlyNonempties
      infos =
        M.fromList $
          map ((name . originalItem . grid . annotatedGrid &&& id) . process) onlyNonempties
  return $
    RecognizerAutomatons
      infos
      (mkEntityLookup rotatedGrids)
 where
  extractedItems = map (uncurry ExtractedArea . fmap (extractor . structure) . dupe) rawGrids

  process g = StructureInfo g entGrid countsMap
   where
    entGrid = extractedGrid $ grid g
    countsMap = histogram . catMaybes . NE.toList $ allMembersNE entGrid

extractOrientedGrid ::
  ExtractedArea b a ->
  AbsoluteDir ->
  StructureWithGrid b a
extractOrientedGrid (ExtractedArea x neGrid) d =
  StructureWithGrid d w $
    ExtractedArea x $
      applyOrientationTransformNE (Orientation d False) neGrid
 where
  w = RowWidth . rectWidth . getNEGridDimensions $ neGrid

-- |
-- At this point, we have already ensured that orientations
-- redundant by rotational symmetry have been excluded
-- (i.e. at Scenario validation time).
extractGrids ::
  ExtractedArea b a ->
  [StructureWithGrid b a]
extractGrids x =
  map (extractOrientedGrid x) orientations
 where
  orientations = Set.toList $ recognize $ originalItem x

-- | The output list of 'FoundStructure' records is not yet
-- vetted; the 'ensureStructureIntact' function will subsequently
-- filter this list.
lookupStaticPlacements ::
  StaticStructureInfo b a ->
  [FoundStructure b a]
lookupStaticPlacements (StaticStructureInfo theAutomatons thePlacements) =
  concatMap f $ M.toList thePlacements
 where
  definitionMap = theAutomatons ^. originalStructureDefinitions

  f (subworldName, locatedList) = mapMaybe g locatedList
   where
    g (LocatedStructure (OrientedStructure theName d) loc) = do
      sGrid <- M.lookup theName definitionMap
      return $
        PositionedStructure (Cosmic subworldName loc) $
          extractOrientedGrid (grid $ annotatedGrid sGrid) d

-- | Matches definitions against the placements.
-- Fails fast (short-circuits) if a non-matching
-- cell is encountered.
--
-- Returns 'Nothing' if there is no discrepancy between the match subject and world content.
-- Returns the first observed mismatch cell otherwise.
ensureStructureIntact ::
  (Monad s, Hashable a) =>
  FoundRegistry b a ->
  GenericEntLocator s a ->
  FoundStructure b a ->
  s (Maybe (StructureIntactnessFailure a))
ensureStructureIntact registry entLoader (PositionedStructure upperLeft (StructureWithGrid _ _ (ExtractedArea _ g))) = do
  fmap leftToMaybe . runExceptT $ mapM checkLoc allLocPairs
 where
  gridArea = getNEGridDimensions g
  checkLoc (maybeTemplateEntity, loc) =
    forM_ maybeTemplateEntity $ \x -> do
      e <- lift $ entLoader loc

      forM_ (M.lookup loc $ foundByLocation registry) $ \s ->
        errorPrefix
          . AlreadyUsedBy
          . distillLabel
          $ structureWithGrid s

      unless (e == Just x)
        . errorPrefix
        $ DiscrepantEntity
        $ EntityDiscrepancy x e
   where
    errorPrefix =
      except
        . Left
        . StructureIntactnessFailure (loc ^. planar) gridArea

  f = fmap ((upperLeft `offsetBy`) . asVector . coordsToLoc) . swap
  allLocPairs = mapWithCoordsNE (curry f) g