packages feed

anitomata-0.1.0.0: test-suite/Test/AnitomataSpec.hs

{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-}
{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE LambdaCase #-}
module Test.AnitomataSpec
  ( spec
  ) where

import Anitomata
import Control.Exception (ErrorCall(..), evaluate, try)
import Data.Function ((&), on)
import Data.Functor ((<&>))
import Data.Kind (Type)
import Data.Maybe (isJust, isNothing)
import Data.Proxy (Proxy(Proxy))
import Data.Semigroup (Semigroup(sconcat))
import GHC.TypeLits (KnownNat, Nat, natVal)
import Prelude
import Test.Hspec (Spec, describe, parallel)
import Test.Hspec.QuickCheck (prop)
import Test.QuickCheck
  ( Arbitrary(arbitrary), NonNegative(getNonNegative), Positive(Positive, getPositive), (.&&.)
  , (===), (==>), Gen, chooseInt, frequency, ioProperty, listOf1, mapSize, oneof, vectorOf
  )

import Data.List qualified as L
import Data.List.NonEmpty qualified as NonEmpty
import Data.Vector qualified as V
import Data.Vector.Generic qualified as G

spec :: Spec
spec = parallel do
  describe "Foo.Data.Anim" do
    prop "Initial source rect is correct" \case
      FiniteTestAnim (TestAnim a) ->
        animFrame a === case animSlice a of
          AnimSlice { animSliceDir = dir, animSliceFrames = frames }
            | dir == AnimDirForward -> V.head frames
            | otherwise -> V.last frames

    prop "Finite animations can be stepped to completion" \case
      FiniteTestAnim (TestAnim a) ->
        -- Use the computed total duration as the delta time so that we can
        -- get through the animation quickly.
        iterateAnim dur a
          & fmap fromSteppedAnim
          & L.lookup AnimStatusFinished
          & isJust
        where
        Just AnimMeta { animMetaTotalDur = dur } = animMeta a

    prop "No frames are skipped if delta time is small enough" $ mapSize (`div` 2) \case
      FiniteTestAnimBuilder (TestAnimBuilder ab) ->
        expectedFrames `L.isSubsequenceOf` realFrames
          .&&. length expectedFrames === totalFrameCount
          .&&. iterDur - totalDur >= 0
          .&&. iterDur - totalDur < dt
        where
        expectedFrames = getExpectedFrames a0
        realFrames = getRealFrames $ iterateAnim dt a0
        iterDur = dt * fromIntegral (length realFrames)
        a0 = buildAnim AnimDurationDefault ab
        dt = minFrameDur / 2
        Just AnimMeta
          { animMetaTotalFrameCount = totalFrameCount
          , animMetaTotalDur = totalDur
          , animMetaMinFrameDur = minFrameDur
          } = animMeta a0

    prop "Some frames are skipped if delta time is large enough" \case
      FiniteTestAnimBuilder (TestAnimBuilder ab) ->
        length expectedFrames >= length realFrames
          .&&. iterDur - totalDur >= 0
          .&&. iterDur - totalDur < dt
        where
        expectedFrames = getExpectedFrames a0
        realFrames = getRealFrames $ iterateAnim dt a0
        iterDur = dt * fromIntegral (length realFrames)
        a0 = buildAnim AnimDurationDefault ab
        dt = maxFrameDur * 2
        Just AnimMeta
          { animMetaTotalDur = totalDur
          , animMetaMaxFrameDur = maxFrameDur
          } = animMeta a0

    prop "Duration of finite animations can be scaled" \case
      (FiniteTestAnimBuilder (TestAnimBuilder ab), Positive scaleFactor) ->
        scaleFactor * iterDurDefault === iterDurScaled
          .&&. scaleFactor * totalDurDefault === totalDurScaled
          .&&. scaleFactor * minFrameDurDefault === minFrameDurScaled
          .&&. scaleFactor * maxFrameDurDefault === maxFrameDurScaled
          .&&. totalFrameCountDefault === totalFrameCountScaled
        where
        stepCountScaled = length $ iterateAnim dtScaled aScaled
        iterDurScaled = dtScaled * fromIntegral stepCountScaled
        aScaled = buildAnim (AnimDurationScaled scaleFactor) ab
        dtScaled = maxFrameDurScaled
        Just AnimMeta
          { animMetaTotalFrameCount = totalFrameCountScaled
          , animMetaTotalDur = totalDurScaled
          , animMetaMinFrameDur = minFrameDurScaled
          , animMetaMaxFrameDur = maxFrameDurScaled
          } = animMeta aScaled

        stepCountDefault = length $ iterateAnim dtDefault aDefault
        iterDurDefault = dtDefault * fromIntegral stepCountDefault
        aDefault = buildAnim AnimDurationDefault ab
        dtDefault = maxFrameDurDefault
        Just AnimMeta
          { animMetaTotalFrameCount = totalFrameCountDefault
          , animMetaTotalDur = totalDurDefault
          , animMetaMinFrameDur = minFrameDurDefault
          , animMetaMaxFrameDur = maxFrameDurDefault
          } = animMeta aDefault

    prop "Total duration of finite animations can be set while respecting relative frame timings" \case
      (FiniteTestAnimBuilder (TestAnimBuilder ab), Positive newTotalDur) ->
        totalDur === newTotalDur
          .&&. iterDur - newTotalDur >= 0
          .&&. iterDur - newTotalDur < dt
          .&&. scaleFactor * minFrameDurDefault === minFrameDur
          .&&. scaleFactor * maxFrameDurDefault === maxFrameDur
          .&&. totalFrameCountDefault === totalFrameCount
        where
        stepCount = length $ iterateAnim dt a'
        iterDur = dt * fromIntegral stepCount
        a' = buildAnim (AnimDurationTotal newTotalDur) ab
        dt = maxFrameDur
        Just AnimMeta
          { animMetaTotalFrameCount = totalFrameCount
          , animMetaTotalDur = totalDur
          , animMetaMinFrameDur = minFrameDur
          , animMetaMaxFrameDur = maxFrameDur
          } = animMeta a'

        scaleFactor = newTotalDur / totalDurDefault

        a = buildAnim AnimDurationDefault ab
        Just AnimMeta
          { animMetaTotalFrameCount = totalFrameCountDefault
          , animMetaTotalDur = totalDurDefault
          , animMetaMinFrameDur = minFrameDurDefault
          , animMetaMaxFrameDur = maxFrameDurDefault
          } = animMeta a

    prop "Constant frame duration of finite animations can be set" \case
      (FiniteTestAnimBuilder (TestAnimBuilder ab), Positive frameDur) ->
        iterDur - frameDur * fromIntegral totalFrameCount >= 0
          .&&. iterDur - frameDur * fromIntegral totalFrameCount < dt
          .&&. minFrameDur === frameDur
          .&&. maxFrameDur === frameDur
          .&&. totalFrameCountDefault === totalFrameCount
        where
        stepCount = length $ iterateAnim dt a'
        iterDur = dt * fromIntegral stepCount
        a' = buildAnim (AnimDurationEachFrame frameDur) ab
        dt = maxFrameDur
        Just AnimMeta
          { animMetaTotalFrameCount = totalFrameCount
          , animMetaMinFrameDur = minFrameDur
          , animMetaMaxFrameDur = maxFrameDur
          } = animMeta a'

        a = buildAnim AnimDurationDefault ab
        Just AnimMeta
          { animMetaTotalFrameCount = totalFrameCountDefault
          } = animMeta a

    prop "Total duration of finite animations can be set via constant frame timing" \case
      (FiniteTestAnimBuilder (TestAnimBuilder ab), Positive newTotalDur) ->
        totalDur === newTotalDur
          .&&. iterDur - newTotalDur >= 0
          .&&. iterDur - newTotalDur < dt
          .&&. minFrameDur === frameDur
          .&&. maxFrameDur === frameDur
          .&&. totalFrameCountDefault === totalFrameCount
        where
        stepCount = length $ iterateAnim dt a'
        iterDur = dt * fromIntegral stepCount
        a' = buildAnim (AnimDurationEachFrameFromTotal newTotalDur) ab
        dt = maxFrameDur
        Just AnimMeta
          { animMetaTotalFrameCount = totalFrameCount
          , animMetaTotalDur = totalDur
          , animMetaMinFrameDur = minFrameDur
          , animMetaMaxFrameDur = maxFrameDur
          } = animMeta a'

        frameDur = newTotalDur / fromIntegral totalFrameCount

        a = buildAnim AnimDurationDefault ab
        Just AnimMeta
          { animMetaTotalFrameCount = totalFrameCountDefault
          } = animMeta a

    prop "Stepping a completed animation is idempotent" \case
      (FiniteTestAnim (TestAnim a), Positive dt) ->
        ((==) `on` animFrame . steppedAnimValue) sa $ stepAnim dt $ steppedAnimValue sa
        where
        sa = last $ iterateAnim dt a

    prop "Infinite animations do not have countable metadata" \case
      (InfiniteTestAnimBuilder (TestAnimBuilder ab), TestAnimDuration ad) ->
        validDur ad ==> isNothing $ animMeta $ buildAnim ad ab

    prop "Infinite animations cannot override total duration" \case
      (InfiniteTestAnimBuilder (TestAnimBuilder ab), TestAnimDuration ad) ->
        invalidInfiniteDur ad ==> ioProperty do
          try (evaluate $ buildAnim ad ab) >>= \case
            Left (ErrorCall msg) -> pure $ "buildAnim: " `L.isPrefixOf` msg
            Right {} -> pure False

    prop "Repeating a finite animation some finite number of times" \case
      (TestAnimSlice as, (TestAnimRepeat ar) :: TestAnimRepeat 1 0, TestAnimDuration ad) ->
        getExpectedFrames a === take (succ n * sliceLen) (cycle $ getSliceFrames as)
        where
        a = buildAnim ad ab
        ab = repeatAnim ar $ fromAnimSlice as
        sliceLen = V.length $ animSliceFrames as
        n = case ar of
          AnimRepeatCount x -> x
          AnimRepeatForever -> error "impossible"

    prop "An infinite animation is \"infinite\"" \case
      (TestAnimSlice as, TestAnimDuration ad, Positive n) ->
        validDur ad && n > sliceLen ==>
        take (n * sliceLen) (getExpectedFrames a) ===
          take (n * sliceLen) (cycle $ getSliceFrames as)
        where
        a = buildAnim ad ab
        ab = repeatAnim AnimRepeatForever $ fromAnimSlice as
        sliceLen = V.length $ animSliceFrames as

    prop "An infinite pingponging animation is \"infinite\"" \case
      (TestAnimSlice as, TestAnimDuration ad, Positive n) ->
        validDur ad ==>
        take (n * sliceLen) (getExpectedFrames a) ===
          take (n * sliceLen) (cycle $ sliceFrames <> reverse sliceFrames)
        where
        sliceFrames = getSliceFrames as
        a = buildAnim ad ab
        ab = repeatAnim AnimRepeatForever $ pingpongAnimSlice as
        sliceLen = V.length $ animSliceFrames as

getExpectedFrames :: G.Vector v f => Anim_ v t f -> [f]
getExpectedFrames a0 = foldMap getSliceFrames $ animSequence a0

getSliceFrames :: G.Vector v f => AnimSlice_ v t f -> [f]
getSliceFrames AnimSlice { animSliceDir = dir, animSliceFrames = frames }
  | AnimDirForward <- dir = G.toList frames
  | otherwise = G.toList $ G.reverse frames

getRealFrames :: G.Vector v f => [SteppedAnim_ v t f] -> [f]
getRealFrames = fmap \case
  SteppedAnim { steppedAnimValue = a } -> animFrame a

validDur :: AnimDuration_ t -> Bool
validDur = \case
  AnimDurationDefault -> True
  AnimDurationScaled {} -> True
  AnimDurationTotal {} -> False
  AnimDurationEachFrame {} -> True
  AnimDurationEachFrameFromTotal {} -> False

invalidInfiniteDur :: AnimDuration_ t -> Bool
invalidInfiniteDur = \case
  AnimDurationDefault -> False
  AnimDurationScaled {} -> False
  AnimDurationTotal {} -> True
  AnimDurationEachFrame {} -> False
  AnimDurationEachFrameFromTotal {} -> True

type FiniteTestAnim :: Type
newtype FiniteTestAnim = FiniteTestAnim (TestAnim 1 0)
  deriving newtype (Arbitrary, Show)

type InfiniteTestAnim :: Type
newtype InfiniteTestAnim = InfiniteTestAnim (TestAnim 0 1)
  deriving newtype (Arbitrary, Show)

type TestAnim :: Nat -> Nat -> Type
newtype TestAnim nf ni = TestAnim (Anim_ V.Vector Rational AnimFrame)
  deriving newtype (Show)

instance (KnownNat nf, KnownNat ni) => Arbitrary (TestAnim nf ni) where
  arbitrary :: Gen (TestAnim nf ni)
  arbitrary = do
    TestAnimDuration dur <- arbitrary
    TestAnimBuilder builder <- arbitrary @(TestAnimBuilder nf ni)
    pure $ TestAnim $ buildAnim dur builder

type FiniteTestAnimBuilder :: Type
newtype FiniteTestAnimBuilder = FiniteTestAnimBuilder (TestAnimBuilder 1 0)
  deriving newtype (Arbitrary, Show)

type InfiniteTestAnimBuilder :: Type
newtype InfiniteTestAnimBuilder = InfiniteTestAnimBuilder (TestAnimBuilder 0 1)
  deriving newtype (Arbitrary, Show)

type TestAnimBuilder :: Nat -> Nat -> Type
newtype TestAnimBuilder nf ni = TestAnimBuilder (AnimBuilder_ V.Vector Rational AnimFrame)
  deriving newtype (Semigroup, Show)

instance (KnownNat nf, KnownNat ni) => Arbitrary (TestAnimBuilder nf ni) where
  arbitrary :: Gen (TestAnimBuilder nf ni)
  arbitrary = do
    sconcat . NonEmpty.fromList <$> listOf1 do
      mkBuilder <- genMkAnimBuilder
      TestAnimSlice slice <- arbitrary
      TestAnimRepeat rep <- arbitrary @(TestAnimRepeat nf ni)
      pure $ TestAnimBuilder $ repeatAnim rep $ mkBuilder slice

type TestAnimSlice :: Type
newtype TestAnimSlice = TestAnimSlice (AnimSlice_ V.Vector Rational AnimFrame)
  deriving newtype (Show)

instance Arbitrary TestAnimSlice where
  arbitrary :: Gen TestAnimSlice
  arbitrary = do
    TestAnimDir dir <- arbitrary
    len <- chooseInt (1, 3)
    frameDurs <- do
      positiveDurs <- fmap getPositive <$> vectorOf len arbitrary
      pure $ V.fromListN len positiveDurs
    frames <- do
      rects <- vectorOf len genAnimFrame
      pure $ V.fromListN len rects
    pure $ TestAnimSlice AnimSlice
      { animSliceDir = dir
      , animSliceFrameDurs = frameDurs
      , animSliceFrames = frames
      }

type TestAnimDuration :: Type
newtype TestAnimDuration = TestAnimDuration (AnimDuration_ Rational)
  deriving stock (Show)

instance Arbitrary TestAnimDuration where
  arbitrary :: Gen TestAnimDuration
  arbitrary =
    TestAnimDuration <$> frequency
      [ (4, pure AnimDurationDefault)
      , (1, AnimDurationScaled . getNonNegative <$> arbitrary)
      , (1, AnimDurationTotal . getPositive <$> arbitrary)
      , (1, AnimDurationEachFrame . getNonNegative <$> arbitrary)
      , (1, AnimDurationEachFrameFromTotal . getPositive <$> arbitrary)
      ]

type TestAnimDir :: Type
newtype TestAnimDir = TestAnimDir AnimDir

instance Arbitrary TestAnimDir where
  arbitrary :: Gen TestAnimDir
  arbitrary = TestAnimDir <$> oneof [pure AnimDirForward, pure AnimDirBackward]

type TestAnimRepeat :: Nat -> Nat -> Type
newtype TestAnimRepeat nf ni = TestAnimRepeat AnimRepeat
  deriving newtype (Show)

instance (KnownNat nf, KnownNat ni) => Arbitrary (TestAnimRepeat nf ni) where
  arbitrary :: Gen (TestAnimRepeat nf ni)
  arbitrary =
    TestAnimRepeat <$> frequency
      [ ( fromInteger $ natVal $ Proxy @nf
        , AnimRepeatCount . getNonNegative <$> arbitrary
        )
      , ( fromInteger $ natVal $ Proxy @ni
        , pure AnimRepeatForever
        )
      ]

genMkAnimBuilder :: Gen (AnimSlice_ V.Vector Rational AnimFrame -> AnimBuilder_ V.Vector Rational AnimFrame)
genMkAnimBuilder = arbitrary <&> \case
  False -> fromAnimSlice
  True -> pingpongAnimSlice

genAnimFrame :: Arbitrary a => Gen (AnimFrame_ a)
genAnimFrame = AnimFrame <$> arbitrary <*> arbitrary <*> arbitrary <*> arbitrary

fromSteppedAnim :: SteppedAnim_ v t f -> (AnimStatus, Anim_ v t f)
fromSteppedAnim SteppedAnim { steppedAnimStatus = as, steppedAnimValue = a } = (as, a)