packages feed

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

-- |
-- SPDX-License-Identifier: BSD-3-Clause
module Swarm.Game.Scenario.Topography.Structure.Recognition.Prep (
  mkEntityLookup,
  binTuplesHM,
) where

import Control.Arrow ((&&&))
import Control.Lens.Indexed (imap)
import Data.HashMap.Strict qualified as HM
import Data.HashSet qualified as HS
import Data.Hashable (Hashable)
import Data.Int (Int32)
import Data.List.NonEmpty (NonEmpty)
import Data.List.NonEmpty qualified as NE
import Data.List.Split (wordsBy)
import Data.Maybe (catMaybes, mapMaybe)
import Data.Semigroup (sconcat)
import Data.Tuple (swap)
import Swarm.Game.Scenario.Topography.Grid
import Swarm.Game.Scenario.Topography.Structure.Recognition.Type
import Text.AhoCorasick (makeStateMachine)

-- | Given all candidate structures, explode them into annotated rows.
-- These annotations entail both the row index with the original structure
-- and a reference to the original structure definition.
--
-- This operation may result in multiple entries that contain the same contents
-- (but different annotations), either because the same contents appear
-- in multiple rows within the same structure, or occur across structures.
allStructureRows :: [StructureWithGrid b a] -> [StructureRow b a]
allStructureRows =
  concatMap $ NE.toList . transformRows
 where
  transformRows g = imap (StructureRow g . fromIntegral) rows
   where
    NonEmptyGrid rows = extractedGrid $ entityGrid g

-- | If this entity is encountered in the world,
-- how far left of it and how far right of it do we need to
-- scan the world row to ensure we can recognize every possible
-- structure that features this entity?
mkOffsets :: Int32 -> RowWidth -> InspectionOffsets
mkOffsets pos (RowWidth w) =
  InspectionOffsets
    (subtractPosFrom 0)
    (subtractPosFrom rightMostShapeRowIndex)
 where
  subtractPosFrom minuend = pure $ minuend - pos
  rightMostShapeRowIndex = w - 1

-- | Make the first-phase lookup map, keyed by 'Entity',
-- along with automatons whose key symbols are "Maybe Entity".
--
-- Each automaton in this first layer will attempt to match the
-- underlying world row against all rows within all structures
-- (so long as they contain the keyed entity).
--
-- = Preparation steps
--
-- 1. Consolidate all identical rows across all structures into a map
-- 2. Consolidate all entities across these rows into an entity-keyed lookup map
-- 3. Extract the contiguous chunks from each unique row
-- 4. Put the expected indices of these chunks into a lookup structure
-- 5. Prepare Aho-Corasick state machines for recognizing these chunks
mkEntityLookup ::
  (Hashable a, Eq a) =>
  [StructureWithGrid b a] ->
  HM.HashMap a (AutomatonInfo b a)
mkEntityLookup grids =
  HM.map mkRowAutomatons rowsByEntityParticipation
 where
  -- Produces an automaton to evaluate whenever a given entity
  -- is encountered.
  mkRowAutomatons neList =
    AutomatonInfo bounds $
      PiecewiseRecognition chunksStateMachine extractedChunksForLookup
   where
    bounds = sconcat $ NE.map expandedOffsets neList

    -- Prepare lookup structure for use with results of the
    -- Aho-Corasick matcher.
    extractedChunksForLookup = NE.map f neList
     where
      f x = RowChunkMatchingReference (myRow x) (mkRightMap x)
      mkRightMap = binTuplesHM . map (chunkContents &&& chunkStartPos) . contiguousChunks

    extractedChunksForStateMachine =
      HS.fromList . concat . NE.toList $
        NE.map (map chunkContents . contiguousChunks) neList

    -- We wrap the entities with 'Just' since the Aho-Corasick
    -- matcher needs to compare against world cells, which are of 'Maybe' type.
    chunksStateMachine =
      makeStateMachine $
        map (NE.toList . fmap Just &&& id) $
          HS.toList extractedChunksForStateMachine

  -- The values of this map are guaranteed to contain only one
  -- entry per row of each structure, even if some of those
  -- rows contain repetition of the same entity.
  rowsByEntityParticipation =
    binTuplesHM
      . map (myEntity &&& id)
      . concatMap explodeRowEntities
      $ structureRowsByContent

  -- Consolidate all identical rows, whether those rows appear in
  -- same structure or a different structures.
  structureRowsByContent =
    map (\(x, y) -> ConsolidatedRowReferences x y . gridWidth . wholeStructure $ NE.head y)
      . HM.toList
      . binTuplesHM
      . map (rowContent &&& id)
      $ allStructureRows grids

-- | Utilizes the convenient 'wordsBy' function
-- from the "split" package.
getContiguousChunks :: SymbolSequence a -> [PositionedChunk a]
getContiguousChunks rowMembers =
  map mkChunk
    . mapMaybe (NE.nonEmpty . mapMaybe sequenceA)
    . wordsBy (null . snd)
    $ zip [0 :: Int ..] rowMembers
 where
  mkChunk xs = PositionedChunk (fst $ NE.head xs) (NE.map snd xs)

-- | All of the occurrences of each unique entity within a row
-- are consolidated into one record, in which the repetitions are noted.
--
-- The members of "rowMembers" are of 'Maybe' type; the 'Nothing's
-- are dropped but accounted for positionally when indexing the columns.
explodeRowEntities ::
  (Hashable a, Eq a) =>
  ConsolidatedRowReferences b a ->
  [SingleRowEntityOccurrences b a]
explodeRowEntities annotatedRow@(ConsolidatedRowReferences rowMembers _ width) =
  map f $ HM.toList $ binTuplesHM unconsolidatedEntityOccurrences
 where
  chunks = getContiguousChunks $ NE.toList rowMembers

  f (e, occurrences) =
    SingleRowEntityOccurrences annotatedRow e chunks $
      sconcat $
        NE.map deriveEntityOffsets occurrences

  -- Tuples of (entity, rowOccurrenceOfEntity).
  -- Only row members for which an entity exists (is not Nothing)
  -- are retained here.
  unconsolidatedEntityOccurrences =
    map swap
      . catMaybes
      . NE.toList
      $ imap (\idx -> fmap (PositionWithinRow (fromIntegral idx) annotatedRow,)) rowMembers

  deriveEntityOffsets :: PositionWithinRow b a -> InspectionOffsets
  deriveEntityOffsets (PositionWithinRow pos _) = mkOffsets pos width

-- * Util

-- | Place the second element of the tuples into bins by
-- the value of the first element.
binTuplesHM ::
  (Foldable t, Hashable a, Eq a) =>
  t (a, b) ->
  HM.HashMap a (NonEmpty b)
binTuplesHM = foldr f mempty
 where
  f = uncurry (HM.insertWith (<>)) . fmap pure