packages feed

swarm-0.7.0.0: test/unit/TestOverlay.hs

{-# LANGUAGE OverloadedStrings #-}

-- |
-- SPDX-License-Identifier: BSD-3-Clause
--
-- Unit tests for generic grid overlay logic
module TestOverlay (testOverlay) where

import Control.Monad (when)
import Data.Function (on)
import Data.Text (Text)
import Swarm.Game.Location
import Swarm.Game.Scenario.Topography.Area (
  AreaDimensions (AreaDimensions),
 )
import Swarm.Game.Scenario.Topography.Grid
import Swarm.Game.Scenario.Topography.Placement
import Swarm.Game.Scenario.Topography.Structure
import Swarm.Game.Scenario.Topography.Structure.Assembly (
  foldLayer,
 )
import Swarm.Game.Scenario.Topography.Structure.Named
import Swarm.Game.Scenario.Topography.Structure.Overlay
import Test.Tasty
import Test.Tasty.HUnit

debugRenderGrid :: Bool
debugRenderGrid = False

-- * Example grids

-- | Single cell
oneByOneGrid :: [[Int]]
oneByOneGrid = [[0]]

-- | Single row with two columns
oneByTwoGrid :: [[Int]]
oneByTwoGrid = [[5, 6]]

-- | Two rows with two columns
twoByTwoGrid :: [[Int]]
twoByTwoGrid =
  [ [1, 2]
  , [3, 4]
  ]

testOverlay :: TestTree
testOverlay =
  testGroup
    "Overlay"
    [ testGroup
        "Empty grids, base grid at origin"
        [ mkOriginTestCase "Northward" (Location 3 2) (Location 0 2)
        , mkOriginTestCase "Westward" (Location (-7) (-1)) (Location (-7) 0)
        ]
    , testGroup
        "Overlay sequences"
        [ testGroup
            "Horizontal siblings"
            [ mkOverlaySequenceOriginTest
                "negative first west of second"
                [ placeUnshifted "sibling1" (Location (-2) 0) twoByTwoGrid
                , placeUnshifted "sibling2" (Location 0 0) oneByTwoGrid
                ]
                (Location (-2) 0)
            , mkOverlaySequenceOriginTest
                "first east of negative second"
                [ placeUnshifted "sibling1" (Location 0 0) twoByTwoGrid
                , placeUnshifted "sibling2" (Location (-2) 0) oneByTwoGrid
                ]
                (Location (-2) 0)
            ]
        , testGroup
            "Vertical siblings"
            [ mkOverlaySequenceOriginTest
                "positive first south of second"
                [ placeUnshifted "sibling1" (Location 0 2) twoByTwoGrid
                , placeUnshifted "sibling2" (Location 0 0) oneByTwoGrid
                ]
                (Location 0 2)
            , mkOverlaySequenceOriginTest
                "first north of positive second"
                [ placeUnshifted "sibling1" (Location 0 0) twoByTwoGrid
                , placeUnshifted "sibling2" (Location 0 2) oneByTwoGrid
                ]
                (Location 0 2)
            ]
        , testGroup
            "Merge sizes"
            [ testMergedSize
                "merge an offset 1x1 atop a 0x0 base"
                (mkNamedStructure "baseLayer" (Location 0 0) [[]])
                (mkNamedStructure "sibling1" (Location (-1) 1) oneByOneGrid)
                (AreaDimensions 1 1)
            , testMergedSize
                "merge a 2x2 atop a 1x1 with an offset"
                (mkNamedStructure "sibling1" (Location (-1) 1) oneByOneGrid)
                (mkNamedStructure "sibling2" (Location 0 0) twoByTwoGrid)
                (AreaDimensions 3 3)
            ]
        , testGroup
            "Northwesterly offset of first sibling"
            [ mkOverlaySequenceOriginTest
                "positive first south of second"
                [ placeUnshifted "sibling1" (Location (-1) 1) oneByOneGrid
                , placeUnshifted "sibling2" (Location 0 0) twoByTwoGrid
                ]
                (Location (-1) 1)
            ]
        ]
    ]

-- * Test construction
testMergedSize ::
  String ->
  NamedStructure (Maybe Int) ->
  NamedStructure (Maybe Int) ->
  AreaDimensions ->
  TestTree
testMergedSize testLabel ns1 ns2 expectedArea =
  testCase testLabel $ do
    assertEqual "Merged area is wrong" expectedArea mergedSize
 where
  mergedSize =
    computeMergedArea $
      (OverlayPair `on` (area . structure)) ns1 ns2

-- | Base layer is at the origin (0, 0).
mkOriginTestCase ::
  String ->
  Location ->
  Location ->
  TestTree
mkOriginTestCase adjustmentDescription overlayLocation expectedBaseLoc =
  testCase (unwords [adjustmentDescription, "origin adjustment"]) $ do
    assertEqual "Base loc wrong" expectedBaseLoc actualBaseLoc
 where
  baseLayer = PositionedGrid (Location 0 0) (EmptyGrid :: Grid (Maybe ()))
  overlayLayer = PositionedGrid overlayLocation EmptyGrid
  PositionedGrid actualBaseLoc _ = baseLayer <> overlayLayer

mkOverlaySequenceOriginTest ::
  String ->
  [Placed (Maybe Int)] ->
  Location ->
  TestTree
mkOverlaySequenceOriginTest = mkOverlaySequenceTest gridPosition

mkOverlaySequenceTest ::
  (Show a, Eq a) =>
  (PositionedGrid (Maybe Int) -> a) ->
  String ->
  [Placed (Maybe Int)] ->
  a ->
  TestTree
mkOverlaySequenceTest f testLabel overlays expectedBaseLoc =
  testCase testLabel $ do
    when debugRenderGrid $
      renderGridResult eitherResultGrid

    assertEqual "Base loc wrong" (Right expectedBaseLoc) $
      f <$> eitherResultGrid
 where
  baseArea = PositionedGrid (Location 0 0) (EmptyGrid :: Grid (Maybe Int))

  eitherResultGrid = getGridFromMergedStructure <$> eitherResult
  eitherResult =
    foldLayer
      mempty
      baseArea
      overlays
      []

getGridFromMergedStructure :: MergedStructure c -> PositionedGrid c
getGridFromMergedStructure (MergedStructure g _ _) = g

-- | Place an structure at an offset.
-- The structure's local origin is (0, 0).
placeUnshifted ::
  Text ->
  Location ->
  [[a]] ->
  Placed (Maybe a)
placeUnshifted = place (Location 0 0)

-- | Place a structure at an offset.
-- That structure's local origin might not be (0, 0).
place ::
  Location ->
  Text ->
  Location ->
  [[a]] ->
  Placed (Maybe a)
place localOrigin theName placementOffset g =
  Placed (Placement sName (Pose placementOffset defaultOrientation)) $
    mkNamedStructure theName localOrigin g
 where
  sName = StructureName theName

mkNamedStructure ::
  Text ->
  Location ->
  [[a]] ->
  NamedArea (PStructure (Maybe a))
mkNamedStructure theName pos g =
  NamedArea sName mempty mempty s
 where
  sName = StructureName theName
  s =
    Structure
      (PositionedGrid pos $ Just <$> mkGrid g)
      mempty
      mempty
      mempty

renderGridResult :: Either a (PositionedGrid (Maybe Int)) -> IO ()
renderGridResult = mapM_ $ \pg -> do
  print pg
  print $ getRows $ gridContent pg