packages feed

dovin-3.0.1: src/Dovin/Effects.hs

{-# LANGUAGE TemplateHaskell #-}

{-|
Effects are continuous effects, such as "other creatures get +1/+1". Note that
dependencies (613.8) are not implemented, and must be emulated with correct
timestamps if needed.

They are typically added to a card using 'Dovin.Builder.withEffect' or 'Dovin.Actions.addEffect'.
 -}
module Dovin.Effects
  ( effectPTSet
  , effectPTSetF
  , effectPTAdjust
  , effectPTAdjustF
  , effectNoAbilities
  , effectAddAbility
  , effectAddType

  , resolveEffects

  , enabledInPlay

  , viewSelf
  , askCards
  , askSelf
  )
  where

import Dovin.Prelude
import Dovin.Types
import Dovin.Matchers (applyMatcher, matchInPlay)

import Control.Lens (makeLenses, over, view, set)
import qualified Data.HashMap.Strict as M
import qualified Data.Set as S
import Control.Monad.Reader (ask, runReader)
import Control.Monad.State (modify', runState, State, get)
import Data.Maybe (mapMaybe, catMaybes)
import Data.List (sortOn, partition)
import Data.Tuple (swap)

type Pile = [PileEntry]
data PileEntry = PileEntry
  { _peSource :: Card
  , _peTimestamp :: Timestamp
  , _peEffect :: [LayeredEffectPart]
  , _peAppliesTo :: Either (EffectMonad CardMatcher) [CardName]
  }
makeLenses ''PileEntry

-- | Constant variant of 'effectPTSetF'.
effectPTSet :: (Int, Int) -> LayeredEffectPart
effectPTSet = effectPTSetF . const . pure

-- | Layer 7B effect to set the power and toughness of a creature.
effectPTSetF :: (Card -> EffectMonad (Int, Int)) -> LayeredEffectPart
effectPTSetF f = LayeredEffectPart Layer7B $ \c -> do
                   pt <- f c
                   return $ set cardStrength (mkStrength pt) c

-- | Constant variant of 'effectPTAdjustF'
effectPTAdjust :: (Int, Int) -> LayeredEffectPart
effectPTAdjust = effectPTAdjustF . const . pure

-- | Layer 7C effect to adjust the power and toughness of a creature.
effectPTAdjustF :: (Card -> EffectMonad (Int, Int)) -> LayeredEffectPart
effectPTAdjustF f = LayeredEffectPart Layer7C $ \c -> do
                   pt <- f c
                   return $ over cardStrength (mkStrength pt <>) c

-- | Layer 6 effect to add an ability to a card. In practice, it adds adds a
-- new 'CardAttribute'.
effectAddAbility attr = LayeredEffectPart Layer6 (pure . over cardAttributes (S.insert attr))

-- | Layer 6 effect to remove all abilities from a card. This doesn't
-- temporary abilities added by 'addEffect'.
effectNoAbilities = LayeredEffectPart Layer6 (pure . set cardPassiveEffects mempty)

-- | Layer 4 effect to add a type to a card. Since card types are modeled
-- explicitly, it instead adds a new 'CardAttribute'.
effectAddType attr = LayeredEffectPart Layer4 (pure . over cardAttributes (S.insert attr))

-- | Effect enabled definition to apply when a card is in play.
enabledInPlay :: EffectMonad Bool
enabledInPlay = applyMatcher matchInPlay <$> askSelf

-- | The card that is generating the effect being applied.
askSelf :: EffectMonad Card
askSelf = snd <$> ask

-- | Apply a lens to 'askSelf'.
viewSelf x = view x <$> askSelf

-- | Return cards fitting the given matcher.
askCards :: CardMatcher -> EffectMonad [Card]
askCards matcher =
  filter (applyMatcher matcher)
  . M.elems
  . view resolvedCards
  . fst
  <$> ask

-- | Internal algorithm to apply re-calculate the state of the board by applying all effects.
resolveEffects :: GameMonad ()
resolveEffects = do
  -- This just happens to be a convenient place to bump the timestamp. SBE
  -- handling might be a better spot though.
  modifying currentTime (+ 1)

  modify' resetCards
  modify' resolveCounters
  modify' applyEffects

resetCards :: Board -> Board
resetCards board = set resolvedCards (M.map unwrap . view cards $ board) board
  where
    unwrap (BaseCard card) = card

-- Unlike the previous effects system, V3 attempts to better mirror the
-- layering rules while also providing a more flexible API to create more
-- types of effects. Some notable constraints this requires solving for
-- include:
--
-- * The set of cards an effect applies to needs to be fixed in the first
--   layer in which the effect would apply.
-- * Layers can create or remove effects in higher layers (e.g. removing all
--   abilities in layer 6). This means it is not possible to know all effects
--   that will be applied at the start of the algorithm, effects need to be
--   collected layer by layer.
--
-- This algorithm uses the concept of a "pile" of unapplied effects, that is
-- both added to and reduced at each layer.
--
-- 1. For every layer, all cards are checked for effects that would start
--    applying on that layer, and all parts of that effect are added to the
--    pile - alongside the set of cards to apply to. For example, "all
--    creatures are 0/1 and have no abilities" applies on both layers 6 and
--    7B and it will be added to the pile when evaluating layer 6.
-- 2. All sub-effects that apply to the current layer are removed from the
--    pile and evaluated in timestamp order. (Note: dependencies are not
--    implemented yet.)
-- 3. After the final layer, the pile should be empty.
applyEffects :: Board -> Board
applyEffects board =
  let
    f accum v = applyEffectsAtLayer v . collectNewEffectsAtLayer v $ accum
    (newBoard, pile) = foldl f (board, mempty) allLayers
  in

  if null pile then
    newBoard
  else
    error "assertion failed: pile should be empty"

-- Convert card counters and legacy strength modifiers into V3 effects. Note
-- that since counter timestamps are not implemented, card timestamps are used
-- instead.
resolveCounters :: Board -> Board
resolveCounters board =
  let
    newCards = M.map countersToEffect . view resolvedCards $ board
  in

  set resolvedCards newCards board

  where
    unwrap (BaseCard card) = card

    countersToEffect :: Card -> Card
    countersToEffect card =
      let es =
            map (AbilityEffect (view cardTimestamp card) EndOfTurn . replicate 1)
            . catMaybes
            . map (\f -> f card)
            $ [ mkPTEffect . dup . view cardPlusOneCounters
              , mkPTEffect . dup . view cardMinusOneCounters
              , mkPTEffect . toTuple . view cardStrengthModifier
              ]
      in

      over
        cardAbilityEffects
        (es <>)
        card

      where
        toTuple (CardStrength p t) = (p, t)
        mkPTEffect :: (Int, Int) -> Maybe LayeredEffectPart
        mkPTEffect (p, t) =
          -- This is an optimization to not create a large number of no-op
          -- effects.
          if p == 0 && t == 0 then
            Nothing
          else
            Just $ effectPTAdjust (p, t)

        dup x = (x, x)

collectNewEffectsAtLayer :: Layer -> (Board, Pile) -> (Board, Pile)
collectNewEffectsAtLayer layer (board, pile) =
  (
    board,
    sortOn (view peTimestamp) . (pile <>) . concatMap (extractCardEffects layer) . view resolvedCards $ board
  )

  where
    -- Find all effects on a card that begin applying at the given layer.
    extractCardEffects :: Layer -> Card -> Pile
    extractCardEffects layer c =
      let
        passiveEffects =
          map ldToPileEntry
          . view cardPassiveEffects
          $ c
        abilityEffects =
            map aeToPileEntry
          . view cardAbilityEffects
          $ c
      in

      filter
        ((==) layer . minimum . map extractLayer . view peEffect)
        (passiveEffects <> abilityEffects)

      where
        aeToPileEntry :: AbilityEffect -> PileEntry
        aeToPileEntry (AbilityEffect t _ es) =
          PileEntry {
            _peSource = c,
            _peTimestamp = t,
            _peEffect = es,
            _peAppliesTo = Right [view cardName c]
          }

        ldToPileEntry :: LayeredEffectDefinition -> PileEntry
        ldToPileEntry ld =
          PileEntry {
            _peSource = c,
            _peTimestamp = view cardTimestamp c,
            _peEffect = view leEffect ld,
            _peAppliesTo = Left (view leAppliesTo ld)
          }

-- Apply all effects in the pile to the board for the given layer.
applyEffectsAtLayer :: Layer -> (Board, Pile) -> (Board, Pile)
applyEffectsAtLayer layer (board, pile) =
  -- Iterate over all pile entries and attempt to apply/reduce them
  swap . runState (catMaybes <$> mapM (applyEntry layer) pile) $ board

  where
    -- Apply (and remove) any effect parts (may be none) in the entry for the
    -- current layer. If this is the first time a part would apply for this
    -- effect, also resolve the CardMatcher to determine the set of cards to
    -- apply to.
    --
    -- The use of State monad here is perhaps a little weird, but was the best
    -- I could come with to structure the code.
    applyEntry :: Layer -> PileEntry -> State Board (Maybe PileEntry)
    applyEntry layer pe = do
      -- Split the remaining entry effects into those that apply in the current
      -- layer, and those that don't.
      let (parts, remainder) = partition (isLayer layer) (view peEffect pe)

      if null parts then
        -- If no parts apply at current layer, there is nothing to be done.
        return (Just pe)
      else do
        board <- get

        let effectEnv = (board, view peSource pe)

        -- If the entire effect hasn't previously decided which set of cards to
        -- apply to, it does so here.
        let cns = either (resolveAppliesTo effectEnv) id (view peAppliesTo pe)

        -- Look up the affected cards in the current board state.
        let cs = mapMaybe (\cn -> M.lookup cn (view resolvedCards board)) cns

        when (length cs /= length cns)
          -- Since SBEs don't apply while we are applying effects, there should
          -- be no opportunity for a card to be removed.
          (error "assertion failed: effected card did not exist on board")

        -- Apply the effect part to the matched cards
        let newCs = map (applyEffectParts effectEnv parts) cs

        -- Update the board state with the newly updated cards.
        modifying
          resolvedCards
          (M.union . M.fromList . indexBy (view cardName) $ newCs)

        return $
          if null remainder then
            Nothing
          else
            Just
            -- The set of cards needs to be cached here for use by future
            -- parts. It should not be recalculated per 613.6.
            . set peAppliesTo (Right cns)
            -- We also remove the applied parts (the current layer) from the
            -- effect.
            . set peEffect remainder
            $ pe

    resolveAppliesTo :: EffectMonadEnv -> EffectMonad CardMatcher -> [CardName]
    resolveAppliesTo (board, source) m =
        map (view cardName)
      . filter (applyMatcher $ runReader m (board, source))
      . M.elems
      . view resolvedCards
      $ board

    applyEffectParts :: EffectMonadEnv -> [LayeredEffectPart] -> Card -> Card
    applyEffectParts env es target =
      foldl
        (\t (LayeredEffectPart _ eff) -> runReader (eff t) env)
        target
        es

indexBy :: (a -> b) -> [a] -> [(b, a)]
indexBy f = map ((,) <$> f <*> id)

extractLayer (LayeredEffectPart l _) = l

isLayer :: Layer -> LayeredEffectPart -> Bool
isLayer l p = l == extractLayer p