packages feed

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

{-# LANGUAGE OverloadedStrings #-}

-- |
-- SPDX-License-Identifier: BSD-3-Clause
--
-- Symmetry analysis for structure recognizer.
module Swarm.Game.Scenario.Topography.Structure.Recognition.Symmetry where

import Control.Monad (forM_, when)
import Data.List.NonEmpty (NonEmpty)
import Data.List.NonEmpty qualified as NE
import Data.Map qualified as M
import Data.Set (Set)
import Data.Set qualified as Set
import Data.Text qualified as T
import Swarm.Game.Scenario.Topography.Placement (Orientation (..), applyOrientationTransformNE)
import Swarm.Game.Scenario.Topography.Structure.Named (recognize)
import Swarm.Game.Scenario.Topography.Structure.Recognition.Static (RotationalSymmetry (..), SymmetryAnnotatedGrid (..))
import Swarm.Game.Scenario.Topography.Structure.Recognition.Type
import Swarm.Language.Syntax.Direction (AbsoluteDir (DSouth, DWest), CoordinateOrientation, getCoordinateOrientation)
import Swarm.Util (commaList, histogram, showT)

data RedundantOrientations
  = TwoFoldRedundancy (NonEmpty CoordinateOrientation)
  | FourFoldRedundancy (Set AbsoluteDir)

renderRedundancy :: RedundantOrientations -> T.Text
renderRedundancy = \case
  TwoFoldRedundancy redundantOrientations ->
    T.unwords
      [ "Redundant"
      , commaList $ map showT $ NE.toList redundantOrientations
      , "orientations supplied with two-fold symmetry."
      ]
  FourFoldRedundancy _xs ->
    T.unwords
      [ "Redundant orientations supplied; with four-fold symmetry, just supply 'north'."
      ]

-- | Warns if any recognition orientations are redundant
-- by rotational symmetry.
-- We can accomplish this by testing only two rotations:
--
-- 1. Rotate 90 degrees. If identical to the original
--    orientation, then has 4-fold symmetry and we don't
--    need to check any other orientations.
--    Warn if more than one recognition orientation was supplied.
-- 2. Rotate 180 degrees.  At best, we may now have
--    2-fold symmetry.
--    Warn if two opposite orientations were supplied.
checkSymmetry ::
  Eq a =>
  ExtractedArea b a ->
  Either RedundantOrientations (SymmetryAnnotatedGrid (ExtractedArea b a))
checkSymmetry x@(ExtractedArea origObject originalRows) = do
  case symmetryType of
    FourFold ->
      when (Set.size suppliedOrientations > 1) . Left $
        FourFoldRedundancy suppliedOrientations
    TwoFold ->
      forM_ (NE.nonEmpty redundantOrientations) $
        Left . TwoFoldRedundancy
     where
      redundantOrientations =
        map fst
          . filter ((> 1) . snd)
          . M.toList
          . histogram
          . map getCoordinateOrientation
          $ Set.toList suppliedOrientations
    _ -> return ()

  return $ SymmetryAnnotatedGrid symmetryType x
 where
  symmetryType
    | quarterTurnRows == originalRows = FourFold
    | halfTurnRows == originalRows = TwoFold
    | otherwise = NoSymmetry

  quarterTurnRows = applyOrientationTransformNE (Orientation DWest False) originalRows
  halfTurnRows = applyOrientationTransformNE (Orientation DSouth False) originalRows

  suppliedOrientations = recognize origObject