packages feed

swarm-0.6.0.0: src/swarm-engine/Swarm/Game/Step/Flood.hs

{-# LANGUAGE TemplateHaskell #-}

-- |
-- SPDX-License-Identifier: BSD-3-Clause
--
-- Implementation of the 'Swarm.Language.Syntax.Volume' command for robots.
--
-- Note: If the robot is currently on an unwalkable cell (which may happen in
-- the case of teleportation or if an entity is placed or pushed into its cell),
-- the volume shall be zero.
module Swarm.Game.Step.Flood (
  floodFill,
) where

import Control.Effect.Lens
import Control.Lens (makeLenses, (%~), (&))
import Data.HashSet (HashSet)
import Data.HashSet qualified as HashSet
import Swarm.Game.Location
import Swarm.Game.Step.RobotStepState
import Swarm.Game.Step.Util (checkMoveFailureUnprivileged)
import Swarm.Game.Step.Util.Inspect (getNeighborLocs)
import Swarm.Game.Universe

data FloodParms = FloodParms
  { theSubworld :: SubworldName
  , maxVisits :: Int
  }

data Tracking = Tracking
  { visited :: HashSet Location
  , floodPartition :: FloodPartition
  }

-- | We annotate each visited cell as
-- being part of the boundary or the interior.
-- This lets us:
--
-- 1. Use the interior cell count as a termination condition
-- 2. Handle (eventual) cache invalidation differently for boundary
--    members than interior members.
data FloodPartition = FloodPartition
  { _boundary :: HashSet Location
  , _interior :: HashSet Location
  }

makeLenses ''FloodPartition

-- |
-- == Algorithm
--
-- Explore via DFS using a list as a stack.
-- Each iteration examines a single cell.
--
-- 1. Mark the popped cell as visited, regardless of walkability.
-- 2. Check popped cell for walkability
-- 3. Add all neighbors that aren't already visited, regardless of walkability, to the stack.
--    But unwalkable cells shall not produce neighbors and shall be marked with a boundary/interior distinction.
floodRecursive ::
  HasRobotStepState sig m =>
  Tracking ->
  [Location] ->
  FloodParms ->
  m (Maybe Int)
floodRecursive tracking pending params =
  case pending of
    nextLoc : otherLocs ->
      if interiorCount > maxVisits params
        then return Nothing
        else checkNeighbors nextLoc otherLocs
    [] -> return $ Just interiorCount
 where
  interiorCount = HashSet.size $ _interior $ floodPartition tracking
  checkNeighbors nextLoc otherLocs = do
    isWalkable <- null <$> checkMoveFailureUnprivileged cosmicLoc
    let candidateNeighbors =
          if isWalkable
            then map (view planar) $ getNeighborLocs cosmicLoc
            else []
        visitableNeighbors = filter (not . (`HashSet.member` visited tracking)) candidateNeighbors

        -- It's cheaper to prepend the "visitableNeighbors" list because
        -- it should in general be a shorter list than the "pending" list.
        newPending = visitableNeighbors <> otherLocs

        partitionMutator =
          if isWalkable
            then interior
            else boundary
        newPartition = floodPartition tracking & partitionMutator %~ HashSet.insert nextLoc

        newTracking =
          tracking
            { visited = newVisited
            , floodPartition = newPartition
            }
    floodRecursive newTracking newPending params
   where
    newVisited = HashSet.insert nextLoc $ visited tracking
    cosmicLoc = Cosmic (theSubworld params) nextLoc

floodFill ::
  HasRobotStepState sig m =>
  Cosmic Location ->
  Int ->
  m (Maybe Int)
floodFill (Cosmic swn curLoc) =
  floodRecursive emptyTracking [curLoc] . FloodParms swn
 where
  emptyTracking = Tracking mempty $ FloodPartition mempty mempty