packages feed

dovin 0.1.0.1 → 0.2

raw patch · 49 files changed

+2527/−355 lines, 49 filesdep +splitsetup-changednew-component:exe:mtgtcPVP ok

version bump matches the API change (PVP)

Dependencies added: split

API changes (from Hackage documentation)

- Dovin.Dump: activatePlaneswalker :: Int -> CardName -> GameMonad ()
- Dovin.Dump: copySpell :: CardName -> CardName -> ExceptT String (ReaderT Env (StateT Board (WriterT [(String, Board)] Identity))) ()
- Dovin.Dump: destroy :: CardName -> ExceptT String (ReaderT Env (StateT Board (WriterT [(String, Board)] Identity))) ()
- Dovin.Dump: exile :: CardName -> ExceptT String (ReaderT Env (StateT Board (WriterT [(String, Board)] Identity))) ()
- Dovin.Dump: fight :: CardName -> CardName -> GameMonad ()
- Dovin.Dump: modifyStrength :: (Int, Int) -> CardName -> GameMonad ()
- Dovin.Dump: removeFromPlay :: CardName -> ExceptT String (ReaderT Env (StateT Board (WriterT [(String, Board)] Identity))) ()
- Dovin.Dump: resetStrength :: CardName -> (Int, Int) -> GameMonad ()
- Dovin.Dump: returnToHand :: CardName -> ExceptT String (ReaderT Env (StateT Board (WriterT [(String, Board)] Identity))) ()
- Dovin.Dump: returnToPlay :: CardName -> ExceptT String (ReaderT Env (StateT Board (WriterT [(String, Board)] Identity))) ()
- Dovin.Dump: runVerbose :: GameMonad () -> IO ()
- Dovin.Dump: sacrifice :: CardName -> ExceptT String (ReaderT Env (StateT Board (WriterT [(String, Board)] Identity))) ()
- Dovin.Dump: target :: CardName -> ExceptT String (ReaderT Env (StateT Board (WriterT [(String, Board)] Identity))) ()
- Dovin.Dump: targetInLocation :: CardLocation -> CardName -> ExceptT String (ReaderT Env (StateT Board (WriterT [(String, Board)] Identity))) ()
- Dovin.Dump: triggerStorm :: (Int -> GameMonad ()) -> GameMonad ()
- Dovin.Dump: whenMatch :: CardName -> CardMatcher -> GameMonad () -> GameMonad ()
- Dovin.Dump: with :: () => t1 -> (t1 -> t2) -> t2
- Lib: someFunc :: IO ()
+ Dovin.Actions: activatePlaneswalker :: CardName -> Int -> CardName -> GameMonad ()
+ Dovin.Actions: copySpell :: CardName -> CardName -> ExceptT String (ReaderT Env (StateT Board (WriterT [Step] Identity))) ()
+ Dovin.Actions: destroy :: CardName -> GameMonad ()
+ Dovin.Actions: exile :: CardName -> GameMonad ()
+ Dovin.Actions: fight :: CardName -> CardName -> GameMonad ()
+ Dovin.Actions: fork :: String -> GameMonad () -> GameMonad ()
+ Dovin.Actions: modifyStrength :: (Int, Int) -> CardName -> GameMonad ()
+ Dovin.Actions: sacrifice :: CardName -> GameMonad ()
+ Dovin.Actions: target :: CardName -> GameMonad ()
+ Dovin.Actions: targetInLocation :: CardLocation -> CardName -> GameMonad ()
+ Dovin.Actions: with :: CardName -> (CardName -> GameMonad ()) -> GameMonad ()
+ Dovin.Attributes: defender :: CardAttribute
+ Dovin.Builder: withMinusOneCounters :: Int -> GameMonad () -> GameMonad ()
+ Dovin.Helpers: gameFinished :: GameMonad Bool
+ Dovin.Helpers: matchMinusOneCounters :: Int -> CardMatcher
+ Dovin.Helpers: matchToughness :: Int -> CardMatcher
+ Dovin.Helpers: modifyCardDeprecated :: CardName -> ASetter Card Card a b -> (a -> b) -> GameMonad ()
+ Dovin.Types: Deck :: Location
+ Dovin.Types: Step :: StepIdentifier -> String -> Board -> Step
+ Dovin.Types: Won :: Player -> Phase
+ Dovin.Types: [_cardMinusOneCounters] :: Card -> Int
+ Dovin.Types: [_cardStrengthModifier] :: Card -> CardStrength
+ Dovin.Types: [_currentStep] :: Board -> StepIdentifier
+ Dovin.Types: [_deck] :: Board -> HashMap Player [CardName]
+ Dovin.Types: [_stepId] :: Step -> StepIdentifier
+ Dovin.Types: [_stepLabel] :: Step -> String
+ Dovin.Types: [_stepState] :: Step -> Board
+ Dovin.Types: cardMinusOneCounters :: Lens' Card Int
+ Dovin.Types: cardStrengthModifier :: Lens' Card CardStrength
+ Dovin.Types: currentStep :: Lens' Board StepIdentifier
+ Dovin.Types: data Step
+ Dovin.Types: deck :: Lens' Board (HashMap Player [CardName])
+ Dovin.Types: incrementStep :: StepIdentifier -> StepIdentifier
+ Dovin.Types: mkStep :: StepIdentifier -> String -> Board -> Step
+ Dovin.Types: stepFork :: Lens' Step (Maybe String)
+ Dovin.Types: stepId :: Lens' Step StepIdentifier
+ Dovin.Types: stepLabel :: Lens' Step String
+ Dovin.Types: stepNumber :: Lens' Step Int
+ Dovin.Types: stepState :: Lens' Step Board
+ Dovin.Types: type StepIdentifier = (Maybe String, Int)
+ Dovin.V1: activatePlaneswalker :: CardName -> Int -> CardName -> GameMonad ()
+ Dovin.V1: copySpell :: CardName -> CardName -> ExceptT String (ReaderT Env (StateT Board (WriterT [Step] Identity))) ()
+ Dovin.V1: destroy :: CardName -> GameMonad ()
+ Dovin.V1: exile :: CardName -> GameMonad ()
+ Dovin.V1: fight :: CardName -> CardName -> GameMonad ()
+ Dovin.V1: modifyStrength :: (Int, Int) -> CardName -> GameMonad ()
+ Dovin.V1: sacrifice :: CardName -> GameMonad ()
+ Dovin.V1: target :: CardName -> GameMonad ()
+ Dovin.V1: targetInLocation :: CardLocation -> CardName -> GameMonad ()
+ Dovin.V1: with :: CardName -> (CardName -> GameMonad ()) -> GameMonad ()
+ Dovin.V1: withMinusOneCounters :: Int -> GameMonad () -> GameMonad ()
+ Dovin.V2: gainLife :: Player -> Int -> GameMonad ()
+ Dovin.V2: loseLife :: Player -> Int -> GameMonad ()
+ Dovin.V2: run :: (Step -> Formatter) -> GameMonad () -> IO ()
+ Dovin.V2: setLife :: Player -> Int -> GameMonad ()
+ Dovin.V2: triggerMentor :: CardName -> CardName -> ExceptT String (ReaderT Env (StateT Board (WriterT [Step] Identity))) ()
+ Dovin.V2: view :: MonadReader s m => Getting a s a -> m a
+ Solutions.GuildsOfRavnica3: whenMatch :: CardName -> CardMatcher -> GameMonad () -> GameMonad ()
+ Solutions.GuildsOfRavnica9: triggerStorm :: (Int -> GameMonad ()) -> GameMonad ()
+ Solutions.RavnicaAllegiance3: attributes :: Formatter
+ Solutions.RavnicaAllegiance3: formatter :: () => p -> Formatter
+ Solutions.RavnicaAllegiance3: solution :: GameMonad ()
+ Solutions.RavnicaAllegiance4: attributes :: Formatter
+ Solutions.RavnicaAllegiance4: formatter :: () => p -> Formatter
+ Solutions.RavnicaAllegiance4: solution :: GameMonad ()
+ Solutions.RavnicaAllegiance5: attributes :: Formatter
+ Solutions.RavnicaAllegiance5: formatter :: Step -> Formatter
+ Solutions.RavnicaAllegiance5: solution :: GameMonad ()
+ Solutions.RavnicaAllegiance5: tapWithFreyalise :: ManaString -> CardName -> ExceptT String (ReaderT Env (StateT Board (WriterT [Step] Identity))) ()
+ Solutions.RavnicaAllegiance7: attributes :: Formatter
+ Solutions.RavnicaAllegiance7: formatter :: () => p -> Formatter
+ Solutions.RavnicaAllegiance7: solution :: GameMonad ()
+ Solutions.RavnicaAllegiance7: unblockable :: [Char]
+ Solutions.RavnicaAllegiancePre2: attributes :: Formatter
+ Solutions.RavnicaAllegiancePre2: blocked :: [Char]
+ Solutions.RavnicaAllegiancePre2: formatter :: Step -> Formatter
+ Solutions.RavnicaAllegiancePre2: resolveHarpyTrigger :: ExceptT String (ReaderT Env (StateT Board (WriterT [Step] Identity))) ()
+ Solutions.RavnicaAllegiancePre2: solution :: GameMonad ()
+ Solutions.RivalsOfIxalan7: copyAttributes :: CardName -> CardName -> GameMonad ()
+ Solutions.UltimateMasters: oldFormatter :: (Eq a, Num a) => a -> Formatter
+ Solutions.WarOfTheSpark2: attributes :: Formatter
+ Solutions.WarOfTheSpark2: dinosaur :: [Char]
+ Solutions.WarOfTheSpark2: formatter :: () => p -> Formatter
+ Solutions.WarOfTheSpark2: solution :: GameMonad ()
+ Solutions.WarOfTheSpark2: tricked :: [Char]
- Dovin.Actions: step :: String -> GameMonad () -> GameMonad ()
+ Dovin.Actions: step :: String -> GameMonad a -> GameMonad a
- Dovin.Actions: withStateBasedActions :: GameMonad () -> GameMonad ()
+ Dovin.Actions: withStateBasedActions :: GameMonad a -> GameMonad a
- Dovin.Dump: run :: (Int -> Formatter) -> GameMonad () -> IO ()
+ Dovin.Dump: run :: (Step -> Formatter) -> GameMonad () -> IO ()
- Dovin.Dump: triggerMentor :: CardName -> CardName -> ExceptT String (ReaderT Env (StateT Board (WriterT [(String, Board)] Identity))) ()
+ Dovin.Dump: triggerMentor :: CardName -> CardName -> ExceptT String (ReaderT Env (StateT Board (WriterT [Step] Identity))) ()
- Dovin.Helpers: gainAttribute :: CardAttribute -> CardName -> ExceptT String (ReaderT Env (StateT Board (WriterT [(String, Board)] Identity))) ()
+ Dovin.Helpers: gainAttribute :: CardAttribute -> CardName -> ExceptT String (ReaderT Env (StateT Board (WriterT [Step] Identity))) ()
- Dovin.Helpers: loseAttribute :: CardAttribute -> CardName -> ExceptT String (ReaderT Env (StateT Board (WriterT [(String, Board)] Identity))) ()
+ Dovin.Helpers: loseAttribute :: CardAttribute -> CardName -> ExceptT String (ReaderT Env (StateT Board (WriterT [Step] Identity))) ()
- Dovin.Helpers: modifyCard :: CardName -> ASetter Card Card a b -> (a -> b) -> GameMonad ()
+ Dovin.Helpers: modifyCard :: ASetter Card Card a b -> (a -> b) -> CardName -> GameMonad ()
- Dovin.Monad: runMonad :: Board -> GameMonad () -> (Either String (), Board, [(String, Board)])
+ Dovin.Monad: runMonad :: Board -> GameMonad a -> (Either String a, Board, [Step])
- Dovin.Types: Board :: HashMap CardName BaseCard -> [CardName] -> HashMap String Int -> HashMap Player Int -> HashMap Player ManaPool -> Phase -> Board
+ Dovin.Types: Board :: HashMap CardName BaseCard -> [CardName] -> HashMap Player [CardName] -> HashMap String Int -> HashMap Player Int -> HashMap Player ManaPool -> Phase -> StepIdentifier -> Board
- Dovin.Types: Card :: CardName -> (Player, Location) -> CardAttributes -> CardAttributes -> CardStrength -> Int -> Int -> [CardEffect] -> Int -> Card
+ Dovin.Types: Card :: CardName -> (Player, Location) -> CardAttributes -> CardAttributes -> CardStrength -> CardStrength -> Int -> Int -> [CardEffect] -> Int -> Int -> Card
- Dovin.Types: type GameMonad a = (ExceptT String (ReaderT Env (StateT Board (WriterT [(String, Board)] Identity)))) a
+ Dovin.Types: type GameMonad a = (ExceptT String (ReaderT Env (StateT Board (WriterT [Step] Identity)))) a
- Dovin.V1: activate :: ManaString -> CardName -> ExceptT String (ReaderT Env (StateT Board (WriterT [(String, Board)] Identity))) ()
+ Dovin.V1: activate :: ManaString -> CardName -> ExceptT String (ReaderT Env (StateT Board (WriterT [Step] Identity))) ()
- Dovin.V1: step :: String -> GameMonad () -> GameMonad ()
+ Dovin.V1: step :: String -> GameMonad a -> GameMonad a
- Dovin.V1: trigger :: CardName -> ExceptT String (ReaderT Env (StateT Board (WriterT [(String, Board)] Identity))) ()
+ Dovin.V1: trigger :: CardName -> ExceptT String (ReaderT Env (StateT Board (WriterT [Step] Identity))) ()
- Dovin.V1: withStateBasedActions :: GameMonad () -> GameMonad ()
+ Dovin.V1: withStateBasedActions :: GameMonad a -> GameMonad a
- Solutions: all :: [([Char], GameMonad (), Int -> Formatter)]
+ Solutions: all :: [([Char], GameMonad (), Step -> Formatter)]
- Solutions.ChannelFireball: flashbackSnapped :: ManaPool -> CardName -> ExceptT String (ReaderT Env (StateT Board (WriterT [(String, Board)] Identity))) ()
+ Solutions.ChannelFireball: flashbackSnapped :: ManaPool -> CardName -> ExceptT String (ReaderT Env (StateT Board (WriterT [Step] Identity))) ()
- Solutions.ChannelFireball: formatter :: (Eq a, Num a) => a -> Formatter
+ Solutions.ChannelFireball: formatter :: Step -> Formatter
- Solutions.ChannelFireball: resolveAetherflux :: Show a => a -> ExceptT String (ReaderT Env (StateT Board (WriterT [(String, Board)] Identity))) ()
+ Solutions.ChannelFireball: resolveAetherflux :: Show a => a -> ExceptT String (ReaderT Env (StateT Board (WriterT [Step] Identity))) ()
- Solutions.ChannelFireball: tapForManaWithTide :: ManaString -> CardName -> ExceptT String (ReaderT Env (StateT Board (WriterT [(String, Board)] Identity))) ()
+ Solutions.ChannelFireball: tapForManaWithTide :: ManaString -> CardName -> ExceptT String (ReaderT Env (StateT Board (WriterT [Step] Identity))) ()
- Solutions.ChannelFireball: withTriggers :: () => (t -> ExceptT String (ReaderT Env (StateT Board (WriterT [(String, Board)] Identity))) a) -> t -> ExceptT String (ReaderT Env (StateT Board (WriterT [(String, Board)] Identity))) ()
+ Solutions.ChannelFireball: withTriggers :: () => (t -> ExceptT String (ReaderT Env (StateT Board (WriterT [Step] Identity))) a) -> t -> ExceptT String (ReaderT Env (StateT Board (WriterT [Step] Identity))) ()
- Solutions.Core19_9: castWithTriggers :: ManaPool -> CardName -> ExceptT String (ReaderT Env (StateT Board (WriterT [(String, Board)] Identity))) ()
+ Solutions.Core19_9: castWithTriggers :: ManaPool -> CardName -> ExceptT String (ReaderT Env (StateT Board (WriterT [Step] Identity))) ()
- Solutions.Example: formatter :: Int -> Formatter
+ Solutions.Example: formatter :: Step -> Formatter
- Solutions.UltimateMasters: formatter :: (Eq a, Num a) => a -> Formatter
+ Solutions.UltimateMasters: formatter :: Step -> Formatter
- Solutions.UltimateMasters: sacrificeToAltar :: ManaString -> CardName -> ExceptT String (ReaderT Env (StateT Board (WriterT [(String, Board)] Identity))) ()
+ Solutions.UltimateMasters: sacrificeToAltar :: ManaString -> CardName -> ExceptT String (ReaderT Env (StateT Board (WriterT [Step] Identity))) ()

Files

README.md view
@@ -30,7 +30,7 @@ ``` haskell module Solutions.Example where -import Dovin.V2+import Dovin  main = run formatter solution @@ -63,10 +63,11 @@       validate (matchAttribute flying) enemy       destroy enemy -formatter :: Int -> Formatter-formatter 2 = manaFormatter-  <> cardFormatter "opponent creatures" (matchLocation (Opponent, Play))-formatter _ = boardFormatter+formatter :: Step -> Formatter+formatter step = case view stepNumber step of+  1 -> manaFormatter+    <> cardFormatter "opponent creatures" (matchLocation (Opponent, Play))+  _ -> boardFormatter  manaFormatter = attributeFormatter $ do   attribute "availble mana" $@@ -112,6 +113,8 @@   interactions. * `UltimateMasters` shows how to track opponent actions. * `ChannelFireball` automatically calculates High Tide mana.+* `WarOfTheSpark2` shows off a hack for effects that depend on attributes of+  other cards (hopefully will have a better fix in API soon!)  ## Development 
Setup.hs view
@@ -1,5 +1,5 @@ import Control.Arrow-import Control.Monad (filterM)+import Control.Monad (filterM, forM) import Data.Char import Data.List import Data.Ord@@ -25,21 +25,39 @@  writeAllSolutions :: IO () writeAllSolutions = do-  modules <- map toModuleName <$> getFilesRecursive "src/Solutions"-  let header = "module Solutions where"+  let baseDir = "src/Solutions" -  let imports = unlines . map ("import qualified " <>) $ modules+  files <- getFilesRecursive baseDir +  let header = unlines+                 [ "module Solutions where"+                 , "import Control.Lens (view)"+                 , "import Dovin.Types (stepNumber)"+                 ]++  modules <- forM files $ \filename -> do+    contents <- readFile $ baseDir </> filename++    let v2 = not $ "Dovin.V1" `isInfixOf` contents++    return (toModuleName filename, v2)++  let imports = unlines . map (("import qualified " <>) . fst) $ modules++   let array = "all = [\n  " <> (intercalate ",\n  " . map formatSolution $ modules) <> "\n  ]"    let contents = unlines [header, imports, array]   rewriteFileEx normal "src/Solutions.hs" contents    where-    formatSolution m = "("+    formatSolution (m, v2) = "("       <> show (drop (length "Solutions.") m) <> ", "       <> m <> ".solution, "-      <> m <> ".formatter)"+      <> (if v2+           then m <> ".formatter"+           else m <> ".formatter . view stepNumber")+      <> ")"  -- All these functions copy+pasted from hspec-discover getFilesRecursive :: FilePath -> IO [FilePath]
app/Main.hs view
@@ -4,6 +4,7 @@ import Dovin  import qualified Solutions+--import Solutions.MTGTC  main :: IO () --main = run formatter solution
dovin.cabal view
@@ -4,10 +4,10 @@ -- -- see: https://github.com/sol/hpack ----- hash: 23abec652853b2a8c66c2399750bd96d11e7232610506070a3b870a252bee00d+-- hash: b16ab68c041fd8e96b59020ab905d0b6ead290ba704b581b99316cb310e5e23f  name:           dovin-version:        0.1.0.1+version:        0.2 synopsis:       A proof assistant for Magic: The Gathering puzzles. description:    Please see the README on GitHub at <https://github.com/xaviershay/dovin#dovin> category:       Games@@ -15,7 +15,7 @@ bug-reports:    https://github.com/xaviershay/dovin/issues author:         Xavier Shay maintainer:     contact@xaviershay.com-copyright:      2018 Xavier Shay+copyright:      2019 Xavier Shay license:        BSD3 license-file:   LICENSE build-type:     Custom@@ -47,7 +47,6 @@       Dovin.Types       Dovin.V1       Dovin.V2-      Lib       Solutions       Solutions.ChannelFireball       Solutions.Core19_9@@ -59,8 +58,14 @@       Solutions.GuildsOfRavnica8       Solutions.GuildsOfRavnica9       Solutions.GuildsOfRavnicaPre2+      Solutions.RavnicaAllegiance3+      Solutions.RavnicaAllegiance4+      Solutions.RavnicaAllegiance5+      Solutions.RavnicaAllegiance7+      Solutions.RavnicaAllegiancePre2       Solutions.RivalsOfIxalan7       Solutions.UltimateMasters+      Solutions.WarOfTheSpark2   other-modules:       Paths_dovin   hs-source-dirs:@@ -72,6 +77,7 @@     , lens     , mtl     , parsec+    , split     , unordered-containers   default-language: Haskell2010 @@ -90,24 +96,54 @@     , lens     , mtl     , parsec+    , split     , unordered-containers   default-language: Haskell2010 +executable mtgtc+  main-is: Main.hs+  other-modules:+      MTGTC+      Paths_dovin+  hs-source-dirs:+      mtgtc+  ghc-options: -threaded -rtsopts -with-rtsopts=-N+  build-depends:+      base >=4.7 && <5+    , containers+    , dovin+    , hashable+    , lens+    , mtl+    , parsec+    , split+    , unordered-containers+  default-language: Haskell2010+ test-suite dovin-test   type: exitcode-stdio-1.0   main-is: Driver.hs   other-modules:       Activate+      ActivatePlaneswalker+      CopySpell       Counter       Damage+      Destroy       Discard       Exert+      Fight       Flashback       Jumpstart+      ModifyStrength       Move       Resolve+      Sacrifice       Spec       StateBasedActions+      Tap+      Target+      TargetInLocation       TestPrelude       TestPrelude.V2       TestSolutions@@ -124,6 +160,7 @@     , lens     , mtl     , parsec+    , split     , tasty     , tasty-discover     , tasty-hunit
+ mtgtc/MTGTC.hs view
@@ -0,0 +1,753 @@+{-# LANGUAGE TemplateHaskell #-}++-- https://arxiv.org/abs/1904.09828+module MTGTC where++import Dovin.V2+import Dovin.Prelude+import Dovin.Monad+import Data.List (intercalate)++import Control.Lens+import qualified Data.List+import qualified Data.Set+import qualified Data.Ord+import qualified Data.Map as M++import Data.List.Split (splitOneOf)++alice = Opponent+bob = Active++-- Indicates that a card's color text has been changed by Glamerdye+colorHacked = "color-hacked"+phasing = "phasing"+phasedOut = "phasedOut"++shroud = "shroud"++aetherborn = "aetherborn"+basilisk = "basilisk"+cephalid = "cephalid"+demon = "demon"+elf = "elf"+faerie = "fairie"+giant = "giant"+harpy = "harpy"+illusion = "illusion"+juggernaut = "juggernaut"+kavu = "kavu"+leviathan = "leviathan"+myr = "myr"+noggle = "noggle"+orc = "orc"+pegasus = "pegasus"+rhino = "rhino"+sliver = "sliver"++lyurgoyf = "lyurgoyf"+rat = "rat"++tapeTypes =+  [ aetherborn+  , basilisk+  , cephalid+  , demon+  , elf+  , faerie+  , giant+  , harpy+  , illusion+  , juggernaut+  , kavu+  , leviathan+  , myr+  , noggle+  , orc+  , pegasus+  , rhino+  , sliver++  , assassin+  , lyurgoyf+  , rat+  ]++unicodeMappings = M.fromList+  [ (aetherborn, "1 ")+  , (basilisk, "1⃗ ")+  , (cephalid, "1⃖ ")+  , (demon, "1⃗₁")+  , (elf, "1⃖₁")+  , (faerie, "b ")+  , (giant, "b⃗ ")+  , (harpy, "b⃖ ")+  , (illusion, "b⃗₁")+  , (juggernaut, "b⃖₁")+  , (kavu, "b₂")+  , (leviathan, "b₃")+  , (myr, "c ")+  , (noggle, "c⃗ ")+  , (orc, "c⃖ ")+  , (pegasus, "c⃗₁")+  , (rhino, "c⃖₁")+  , (sliver, "c₂")++  , (assassin, "!!")+  , (lyurgoyf, "L ")+  , (rat, "R ")+  ]++assassin = "assassin"+assemblyWorker = "assembly-worker"++white = "white"+blue = "blue"+black = "black"+red = "red"+green = "green"+allColors = [white, blue, black, red, green]++extractColors = Data.Set.toList . Data.Set.intersection (Data.Set.fromList [white, blue, black, red, green]) . view cardAttributes++castWithWildEvocation = castNoRestrictions ""++castNoRestrictions mana name = do+  card <- requireCard name mempty+  spendMana mana+  modifyCard (location . _2) (const Stack) name+  modifying+    stack+    ((:) name)+  when+    (hasAttribute sorcery card || hasAttribute instant card) $+    modifying+      (counters . at storm . non 0)+      (+ 1)++draw :: CardName -> GameMonad ()+draw expectedName = do+  active <- view envActor+  s <- use $ deck . at active . non mempty++  case s of+    [] -> throwError $ "deck is empty, expecting " <> expectedName+    (name:cs) -> do+      unless (name == expectedName) $+        throwError $ "unexpected top of deck: expected "+                       <> expectedName+                       <> ", got "+                       <> name+      moveTo Hand expectedName+      modifying+        (deck . at alice . non mempty)+        (drop 1)++mill :: CardName -> GameMonad ()+mill expectedName = do+  active <- view envActor+  s <- use $ deck . at active . non mempty++  case s of+    [] -> throwError $ "deck is empty, expecting " <> expectedName+    (name:cs) -> do+      unless (name == expectedName) $+        throwError $ "unexpected top of deck: expected "+                       <> expectedName+                       <> ", got "+                       <> name+      moveTo Graveyard expectedName+      modifying+        (deck . at active . non mempty)+        (drop 1)++drawTop = do+  active <- view envActor+  s <- use $ deck . at active . non mempty++  case s of+    [] -> throwError $ "deck is empty"+    (name:cs) -> do+      moveTo Hand name+      modifying+        (deck . at active . non mempty)+        (drop 1)++phaseCards owner = do+  forCards (matchAttribute phasing <> matchController owner) $ \cn -> do+    c <- requireCard cn mempty++    if hasAttribute phasedOut c then+      loseAttribute phasedOut cn+    else+      gainAttribute phasedOut cn++findM :: (a -> GameMonad ()) -> [a] -> GameMonad a+findM f [] = throwError "No matching element"+findM f (x:xs) = (f x >> return x) `catchError` (\_ -> findM f xs)++-- Technically the card should never hit the graveyard, but that's not relevant+-- in this proof. Instead, move from graveyard to bottom of deck.+wheelOfSunAndMoon name = do+  active <- view envActor+  validate (matchLocation (active, Graveyard)) name+  moveTo Deck name+  modifying+    (deck . at active . non mempty)+    (\xs -> xs ++ [name])++lookupSingleCard :: CardMatcher -> GameMonad Card+lookupSingleCard matcher = do+  matchingCs <- lookupCards matcher++  case matchingCs of+    [] -> throwError $ "No matches: " <> show matcher+    [x] -> return x+    xs -> throwError $ "Ambigious match: " <> (intercalate ", " . map (view cardName) $ xs)++lookupCards :: CardMatcher -> GameMonad [Card]+lookupCards matcher = do+  cs <- allCards++  return $ filter (applyMatcher matcher) cs++whenNotHalted m = do+  halted <- gameFinished++  unless halted m++whenState f m = do+  result <- (f >> pure True) `catchError` (const $ pure False)++  when result m++data State = Q1 | Q2 deriving (Show, Eq)++data Rule = Rule+  { _ruleState :: State+  , _ruleNumber :: Int+  , _ruleTrigger :: CardAttribute+  , _ruleAttributes :: [CardAttribute]+  }+makeLenses ''Rule++mkRule s n trigger attributes = Rule+  { _ruleState = s+  , _ruleNumber = n+  , _ruleTrigger = trigger+  , _ruleAttributes = attributes+  }++instance Show Rule where+  show = triggeringCreature++triggeringCreature rule =+  let baseCreature = if tapped `elem` view ruleAttributes rule then+                       "Xathrid Necromancer"+                     else+                       "Rotlung Reanimator"+  in++  baseCreature <> " " <> show (view ruleState rule) <> " " <> show (view ruleNumber rule)+++rules =+  [ mkRule Q1 1 aetherborn [sliver, white]+  , mkRule Q1 2 basilisk [green, elf]+  , mkRule Q1 3 cephalid [sliver, white]+  , mkRule Q1 4 demon [green, aetherborn]+  , mkRule Q1 5 elf [white, demon]+  , mkRule Q1 6 faerie [green, harpy]+  , mkRule Q1 7 giant [green, juggernaut]+  , mkRule Q1 8 harpy [white, faerie]+  , mkRule Q1 9 illusion [green, faerie]+  , mkRule Q1 10 juggernaut [white, illusion]+  , mkRule Q1 11 kavu [white, leviathan, tapped]+  , mkRule Q1 12 leviathan [white, illusion, tapped]+  , mkRule Q1 13 myr [white, basilisk, tapped]+  , mkRule Q1 14 noggle [green, orc]+  , mkRule Q1 15 orc [white, pegasus]+  , mkRule Q1 16 pegasus [green, rhino, tapped]+  , mkRule Q1 17 rhino [assassin, blue]+  , mkRule Q1 18 sliver [green, cephalid]+  , mkRule Q2 1 aetherborn [green, cephalid]+  , mkRule Q2 2 basilisk [green, cephalid]+  , mkRule Q2 3 cephalid [white, basilisk]+  , mkRule Q2 4 demon [green, elf]+  , mkRule Q2 5 elf [white, aetherborn]+  , mkRule Q2 6 faerie [green, kavu, tapped]+  , mkRule Q2 7 giant [green, harpy]+  , mkRule Q2 8 harpy [white, giant]+  , mkRule Q2 9 illusion [green, juggernaut]+  , mkRule Q2 10 juggernaut [white, giant]+  , mkRule Q2 11 kavu [green, faerie, tapped]+  , mkRule Q2 12 leviathan [green, juggernaut]+  , mkRule Q2 13 myr [green, orc]+  , mkRule Q2 14 noggle [green, orc]+  , mkRule Q2 15 orc [white, noggle]+  , mkRule Q2 16 pegasus [green, sliver]+  , mkRule Q2 17 rhino [white, sliver, tapped]+  , mkRule Q2 18 sliver [white, myr]+  ]++mappings = M.fromList+  . map (\x -> (head x, x))+  . Data.List.delete rat+  . Data.List.delete lyurgoyf+  . Data.List.delete assassin+  $ tapeTypes++charToType :: Char -> GameMonad CardAttribute+charToType c = do+  case M.lookup c mappings of+    Nothing -> throwError ("Unknown tape symbol: " <> [c])+    Just x -> return x++encodeTape :: String -> GameMonad ()+encodeTape xs = do+  let [ls, (c:_), rs] = splitOneOf "[]" xs++  forM_ (zip (reverse ls) [3..]) $ \(l, s) -> do+    t <- charToType l+    withAttributes [token, green, t] $ addCreature (s, s) ("Initial L" <> show s)++  t <- charToType c+  withAttributes [token, t] $ addCreature (2, 2) ("Initial C")++  forM_ (zip rs [3..]) $ \(l, s) -> do+    t <- charToType l+    withAttributes [token, white, t] $ addCreature (s, s) ("Initial R" <> show s)++  let n = length ls + 2++  withAttributes [green, lyurgoyf, token] $ addCreature (n, n) "Lyurgoyf Left Tape"++  let n = length rs + 2+  withAttributes [white, rat, token] $ addCreature (n, n) "Rat Right Tape"++setup :: String -> GameMonad ()+setup tape = do+  transitionToForced Untap++  as alice $ do+    withLocation Hand $ do+      addSorcery "Infest"++    withLocation Deck $ do+      addInstant "Cleansing Beam"+      addSorcery "Coalition Victory"+      withAttribute black $ addCreature (3, 3) "Soul Snuffers"++      assign+        (deck . at alice)+        (Just ["Cleansing Beam", "Coalition Victory", "Soul Snuffers"])++    withLocation Play $ do+      addArtifact "Mesmeric Orb"+      addAura "Illusory Gains"+      withAttributes allColors $ addLand "Island"+      withAttribute green $ addEnchantment "Choke"+      -- TODO: This should be (2, 2), but then dread of night/infest kills it?+      withAttributes [red, green, white, black, assemblyWorker] $ addCreature (5, 5) "Fungus Sliver"+      withEffect+        matchInPlay+        (pure $ matchAttribute creature <> matchAttribute assemblyWorker)+        (pure . setAttribute shroud)+        $ addEnchantment "Steely Resolve"++  as bob $ do+    withLocation Play $ do+      encodeTape tape+      -- "[p]fr"+      --encodeTape "" 's' "sr"+      --encodeTape "crrffafafaffaffaaaaaaafaaaaaf" 'f' "amamamc"+      addEnchantment "Wild Evocation"++      withEffect+        matchInPlay+        (const $ matchAttribute creature <> matchAttribute black)+        (pure . over cardStrengthModifier (mkStrength (-1, -1) <>))+        $ withAttribute colorHacked+        $ addEnchantment "Dread of Night 1"++      withEffect+        matchInPlay+        (const $ matchAttribute creature <> matchAttribute black)+        (pure . over cardStrengthModifier (mkStrength (-1, -1) <>))+        $ withAttribute colorHacked+        $ addEnchantment "Dread of Night 2"++      withEffect+        matchInPlay+        (const $ matchAttribute creature <> matchAttribute rat)+        (pure . over cardStrengthModifier (mkStrength (1, 1) <>))+        $ addEnchantment "Shared Triumph 1"++      withEffect+        matchInPlay+        (const $ matchAttribute creature <> matchAttribute lyurgoyf)+        (pure . over cardStrengthModifier (mkStrength (1, 1) <>))+        $ addEnchantment "Shared Triumph 2"++      -- TODO: setup this color pallete from black+      forM_ rules $ \rule -> do+        let name = triggeringCreature rule++        let extraAttributes = if view ruleState rule == Q2 then+                                [phasedOut]+                              else+                                []+        withAttributes [red, green, black, white]+          $ withAttributes (phasing:extraAttributes)+          $ addCreature (2, 2) name++      withEffect+        matchInPlay+        (matchLocation . view cardLocation)+        (pure . setAttribute hexproof)+        $ addEnchantment "Privileged Position"++stepCompute :: Int -> GameMonad ()+stepCompute n = do+  runLoop n++runLoop n = do+  turn1 n+  turn2 n++  whenState (validate (matchLocation (alice, Hand)) "Coalition Victory") $ do+    turn3 n+++  whenNotHalted $ do+    turn4 n++turnStep c n l = step ("Cycle " <> show c <> ", Turn " <> show n <> ": " <> l)++turn1 n = do+  turnStep n 1 "Alice: Untap" $ do+    transitionToForced Untap+    phaseCards alice++  deadToken <- turnStep n 1 "Upkeep: Infest" $ do+    transitionTo Upkeep++    as bob $ do+      trigger "Force cast" "Wild Evocation" >> resolveTop++    as alice $ do+      _ <- lookupSingleCard (matchLocation (alice, Hand) <> matchName "Infest")++      castWithWildEvocation "Infest" >> resolveTop+      wheelOfSunAndMoon "Infest"++    forCards (matchInPlay <> matchAttribute creature) $+      modifyCard cardStrengthModifier (mkStrength (-2, -2) <>)++    lookupSingleCard (matchInPlay <> matchAttribute creature <> matchLesserPower 1)++  deadToken' <-+    if (hasAttribute rat deadToken) then+      do+        -- Handle infinite tape on right side+        -- TODO: Trigger and resolve things, don't just create creatures+        as bob $ do+          -- TODO: Illusory gains, though shouldn't be relevant+          withLocation Play $+            withAttributes [rat, white, token] $ addCreature (2, 2) ("Rat " <> show n)++        as alice $ do+          withLocation Play $+            withAttributes [cephalid, black, token] $ addCreature (2, 2) ("Cephalid " <> show n)++        requireCard ("Cephalid " <> show n) (matchLesserPower 1)+    else if (hasAttribute lyurgoyf deadToken) then+      do+        -- Handle infinite tape on right side+        -- TODO: Trigger and resolve things, don't just create creatures+        as bob $ do+          -- TODO: Illusory gains, though shouldn't be relevant+          withLocation Play $+            withAttributes [lyurgoyf, green, token] $ addCreature (2, 2) ("Lyurgoyf " <> show n)++        as alice $ do+          withLocation Play $+            withAttributes [cephalid, black, token] $ addCreature (2, 2) ("Cephalid " <> show n)++        requireCard ("Cephalid " <> show n) (matchLesserPower 1)+    else+      return deadToken++  let matchingRules =+        filter+          (\rule -> hasAttribute (view ruleTrigger rule) deadToken')+          rules++  when (null matchingRules) $+    throwError ("Unknown card died: " <> formatCard deadToken)++  rule <- findM+            (validate (missingAttribute phasedOut) . triggeringCreature)+            matchingRules++  let c = triggeringCreature rule++  turnStep n 1 ("Action: " <> show rule) $ do+    as bob $ do+      trigger (c <> " Trigger") c >> resolveTop++      withLocation Play+        $ withAttributes (token : view ruleAttributes rule)+        $ addCreature (2, 2) ("Token " <> show n)++  turnStep n 1 "Illusory Gains" $ do+    as alice $ do+      trigger "Steal" "Illusory Gains" >> resolveTop+      -- TODO: Better tracking of what Illusory Gains is attached to+      forCards (matchController alice <> matchAttributes [creature, token] <> matchInPlay) $+        move (alice, Play) (bob, Play)+      move (bob, Play) (alice, Play) ("Token " <> show n)++  turnStep n 1 "Draw" $ do+    transitionTo DrawStep+    as alice $ draw "Cleansing Beam"++    -- TODO: Validate alice can't do anything++  turnStep n 1 "EoT: Undo infest -2/-2" $ do+    forCards (matchInPlay <> matchAttribute creature) $+      modifyCard cardStrengthModifier (const $ mkStrength (0, 0))++  turnStep n 1 "Bob: Untap and Phase" $ do+    phaseCards bob++turn2 n = do+  turnStep n 2 "Alice Untap" $ do+    transitionToForced Untap+    phaseCards alice++    as alice $ do+      tappedCard <- lookupCards (matchController alice <> matchAttribute tapped)++      case tappedCard of+        [] -> return ()+        [c] -> do+          let cn = view cardName c+          untap cn+          -- No priority in untap phase, to this doesn't technically go on stack until Upkeep+          trigger "Mill" "Mesmeric Orb"++        cs -> throwError $ "Too many tapped cards: " <> intercalate ", " (map (view cardName) cs)+++  turnStep n 2 "Upkeep: Cleansing Beam" $ do+    transitionTo Upkeep++    as bob $ do+      trigger "Force cast" "Wild Evocation" >> resolveTop++    as alice $ do+      _ <- lookupSingleCard+             (  matchLocation (alice, Hand)+             <> matchName "Cleansing Beam"+             )++      castWithWildEvocation "Cleansing Beam" >> resolveTop+      wheelOfSunAndMoon "Cleansing Beam"++      card <- lookupSingleCard+                (  matchInPlay+                <> matchAttribute creature+                <> missingAttribute shroud+                <> missingAttribute hexproof+                )+      target (view cardName card)++      forCards (+          matchAttribute creature+          <> (foldl (\b a -> matchAttribute a `matchOr` b) (invert mempty) (extractColors card))+        ) $ \cn -> do+          -- TODO: Check for vigor+          modifyCard cardPlusOneCounters (+ 2) cn++  turnStep n 2 "Mill from Mesmeric Orb (if triggered)" $ do+    s <- use $ stack++    as alice $ do+      when (not . null $ s) $ do+        resolve "Mill"+        mill "Coalition Victory"+        wheelOfSunAndMoon "Coalition Victory"++  turnStep n 2 "Alice Draw" $ do+    transitionTo DrawStep+    as alice $ drawTop++  turnStep n 2 "Bob: Untap and Phase" $ do+    phaseCards bob++turn3 n = do+  turnStep n 3 "Upkeep: Coalition Victory" $ do+    transitionToForced Untap+    phaseCards alice++    transitionTo Upkeep++    as bob $ do+      trigger "Force cast" "Wild Evocation" >> resolveTop++    as alice $ do+      _ <- lookupSingleCard+             (  matchLocation (alice, Hand)+             <> matchName "Coalition Victory"+             )++      castWithWildEvocation "Coalition Victory" >> resolveTop++      matches <-+        sequence+          . map (\(c, t) -> (not . null) <$> lookupCards (matchController alice <> matchAttributes [c, t]))+           $ [(x, y) | x <- allColors, y <- [creature, land]]++      if (Prelude.all id matches) then+        throwError "won game"+      else+        wheelOfSunAndMoon "Coalition Victory"++  whenNotHalted $ do+    turnStep n 3 "Alice Draw" $ do+      transitionTo DrawStep+      as alice $ draw "Soul Snuffers"++  turnStep n 3 "Bob: Untap and Phase" $ do+    phaseCards bob++turn4 n = do+  turnStep n 4 "Upkeep: Soul Snuffers" $ do+    transitionToForced Untap+    phaseCards alice+    transitionTo Upkeep++    as bob $ do+      trigger "Force cast" "Wild Evocation" >> resolveTop++    as alice $ do+      _ <- lookupSingleCard+             (  matchLocation (alice, Hand)+             <> matchName "Soul Snuffers"+             )++      castWithWildEvocation "Soul Snuffers" >> resolveTop++      withStateBasedActions $ do+        forCards (matchInPlay <> matchAttribute creature) $+          modifyCard cardMinusOneCounters (+ 1)++      wheelOfSunAndMoon "Soul Snuffers"+++  turnStep n 4 "Alice Draw" $ do+    transitionTo DrawStep+    as alice $ draw "Infest"++  turnStep n 4 "Bob: Untap and Phase" $ do+    phaseCards bob++matchAny = foldl (\b a -> a `matchOr` b) (invert mempty)++tapeFormatter :: Formatter+tapeFormatter board =+  let f matcher = Data.List.sortBy (Data.Ord.comparing tapePosition)+            . filter (applyMatcher $ matcher <> matchAny (map matchAttribute tapeTypes))+             $ cs in++  let leftCs = f (matchAttribute green <> invert (matchToughness 2)) in+  let rightCs = f (matchAttribute white <> invert (matchToughness 2)) in+  let centerCs = f (matchToughness 2) in++  let tapeValid = length centerCs == 1+                  && contiguous (map (view cardToughness) leftCs)+                  && contiguous (map (view cardToughness) rightCs)+                  && (null leftCs || minimum (map (view cardToughness) leftCs) == 3)+                  && (null rightCs || minimum (map (view cardToughness) rightCs) == 3)+                in++  let tapeWithHead = map extractSymbol (reverse leftCs) <> "[" <> map extractSymbol centerCs <> "]" <> map extractSymbol rightCs in++  if tapeValid then+    tapeWithHead+  else+    ""++  where+    cs = let Right value = execMonad board allCards in value+    tapePosition c = view cardPower c++stateFormatter :: Formatter+stateFormatter board =+  let expr = requireCard "Rotlung Reanimator Q1 1" mempty in+  let Right value = execMonad board expr in++  if hasAttribute phasedOut value then+    "Q2: "+  else+    "Q1: "++tapeFormatter2 :: Formatter+tapeFormatter2 board =+  let f matcher = Data.List.sortBy (Data.Ord.comparing tapePosition)+            . filter (applyMatcher $ matcher <> matchAny (map matchAttribute tapeTypes))+             $ cs in++  let leftCs = f (matchAttribute green <> invert (matchToughness 2)) in+  let rightCs = f (matchAttribute white <> invert (matchToughness 2)) in+  let centerCs = f (matchToughness 2) in++  let tapeValid = length centerCs == 1+                  && contiguous (map (view cardToughness) leftCs)+                  && contiguous (map (view cardToughness) rightCs)+                in++  let tapeWithHead =+          formatSymbols (reverse leftCs) <> ">" <>+            formatSymbols centerCs <> " " <>+            formatSymbols rightCs in++  if tapeValid then+    tapeWithHead+  else+    "\ntape invalid"++  where+    cs = let Right value = execMonad board allCards in value+    tapePosition c = view cardPower c+    formatSymbols = intercalate " " . map extractSymbol2++extractSymbol c = if hasAttribute assassin c then 'H' else head . head . Data.Set.toList $ (Data.Set.fromList tapeTypes) `Data.Set.intersection` (view cardAttributes c)++extractSymbol2 :: Card -> String+extractSymbol2 c = +  case M.lookup relevantAttribute unicodeMappings of+    Nothing -> "? "+    Just x -> x++  where+    relevantAttribute =+      head+      .  Data.Set.toList+      $ (Data.Set.fromList tapeTypes)+          `Data.Set.intersection`+          (view cardAttributes c)++matchOwner :: Player -> CardMatcher+matchOwner x = CardMatcher ("owner " <> show x) $+  (==) x . fst . view cardLocation++contiguous xs = Prelude.all (\(x, y) -> y - x == 1) $ zip xs (tail xs)++deckFormatter actor board = "\n      deck: " <> intercalate "" (map (\cn -> "\n        " <> cn) (view (deck . at actor . non mempty) board))+formatter step = case view stepNumber step of+  _ -> tapeFormatter+--    <> cardFormatter "tape (bob)" (matchAny (map matchAttribute tapeTypes) <> matchOwner bob)+--    <> cardFormatter "tape (alice)" (matchAny (map matchAttribute tapeTypes) <> matchOwner alice)+--    <> deckFormatter alice
+ mtgtc/Main.hs view
@@ -0,0 +1,52 @@+module Main where++import Dovin.V2+import Dovin.Prelude+import Dovin.Monad++import qualified MTGTC++import System.Exit (exitFailure, exitSuccess)++main = do+  --let tape = "[s]ssr"+  --let tape = "[p]fr"+  --let tape =  "crrffafafaffaffaaaaaaafaaaaaf[f]amamamc"+  --let tape = "rrffafafaf[f]amamam"+  let tape = "rrffaafaf[f]amam"+  let tape = "rrffaaaaaaaaaaaffaaaaaaaaaaaafaaaaaaaaaaaafaaaaaaaaaaaffaaaaaaaaaffaaaaaf[f]amaaamaaamaaam"+  --let tape = "[s]"++  let (e, initialBoard, _) = runMonad emptyBoard (MTGTC.setup tape)+  putStrLn . myFormatter $ initialBoard+  _ <- foldM runCycle initialBoard [1..]+  putStrLn "Done"++fixupFinalBoard = do+  forCards (matchAttribute MTGTC.assassin) $+    modifyCard cardStrengthModifier (mkStrength (-2, -2) <>)++runCycle :: Board -> Int -> IO Board+runCycle board n = do+  let (e, newBoard, log) = runMonad board (MTGTC.stepCompute n)++  case e of+    Left "won game" -> do+      let (e, finalBoard, log) = runMonad newBoard fixupFinalBoard+      putStrLn ""+      putStrLn . MTGTC.tapeFormatter2 $ finalBoard+      exitSuccess+    Left x -> putStrLn x >> exitFailure+    Right _ -> do+      putStrLn . myFormatter $ newBoard+    --  putStrLn . myFormatter $ newBoard+    --  forM_ log $ \step -> do+    --    putStr $ show (view stepNumber step) <> ". "+    --    putStr $ view stepLabel step+    --    putStrLn . myFormatter . view stepState $ step+      return newBoard++myFormatter =+     MTGTC.stateFormatter+  <> MTGTC.tapeFormatter2+    -- <> cardFormatter "tape" (MTGTC.matchAny (map matchAttribute MTGTC.tapeTypes))
src/Dovin/Actions.hs view
@@ -10,6 +10,7 @@ -} module Dovin.Actions (   step+  , fork   -- * Casting   , cast   , castFromLocation@@ -20,17 +21,27 @@   , resolveTop   , splice   , tapForMana+  , target+  , targetInLocation   -- * Uncategorized   , activate+  , activatePlaneswalker   , attackWith   , combatDamage+  , copySpell   , damage+  , destroy   , discard   , exert+  , exile+  , fight+  , modifyStrength   , moveTo+  , sacrifice   , transitionTo   , transitionToForced   , trigger+  , with   -- * Validations   , validate   , validateCanCastSorcery@@ -132,7 +143,7 @@     (hasAttribute instant card || hasAttribute flash card)     validateCanCastSorcery -  modifyCard name (location . _2) $ const Stack+  modifyCard (location . _2) (const Stack) name    card <- requireCard name mempty @@ -286,6 +297,26 @@        assign stack xs +-- | Sacrifice a permanent.+--+-- > sacrifice "Soldier"+--+-- [Validates]+--+--   * Permanent controlled by current actor.+--   * Permanent is in play.+--+-- [Effects]+--+--   * Card is moved to graveyard. See 'move' for possible alternate effects.+sacrifice :: CardName -> GameMonad ()+sacrifice cn = do+  actor <- view envActor++  validate (matchController actor <> matchInPlay) cn++  moveTo Graveyard cn+ -- | Splices a spell on to a previously cast arcane spell. -- -- > splice "Goryo's Vengeance" "2RR" "Through the Breach"@@ -352,7 +383,7 @@ -- | Triggers an effect of a permanent. Typically you will want to `resolve` -- after triggering. ----- > activate "Draw Card" "Dawn of Hope" >> resolveTop+-- > trigger "Draw Card" "Dawn of Hope" >> resolveTop -- -- [Validates] --@@ -382,6 +413,11 @@     stack     ((:) triggerName) +-- | Helper function to provide a scoped let.+--+-- > with "Angel" $ \cn -> target cn >> destroy cn+with :: CardName -> (CardName -> GameMonad ()) -> GameMonad ()+with x f = f x  -- | Move a card from one location to another. --@@ -434,9 +470,10 @@     gainAttribute summoned name    when (snd from == Play && snd to /= Play) $ do-    modifyCard name cardPlusOneCounters (const 0)-    modifyCard name cardDamage (const 0)-    modifyCard name cardAttributes (const $ view cardDefaultAttributes c)+    modifyCard cardPlusOneCounters (const 0) name+    modifyCard cardDamage (const 0) name+    modifyCard cardAttributes (const $ view cardDefaultAttributes c) name+    modifyCard cardStrengthModifier (const mempty) name    -- These conditionals are acting on the card state _before_ any of the above   -- changes were applied.@@ -445,10 +482,34 @@       loseAttribute exileWhenLeaveStack name       moveTo Exile name   else if snd from == Play && snd to == Graveyard && view cardPlusOneCounters c == 0 && hasAttribute undying c then-    modifyCard name cardPlusOneCounters (+ 1)+    modifyCardDeprecated name cardPlusOneCounters (+ 1)   else-    modifyCard name location (const to)+    modifyCardDeprecated name location (const to) +-- | Target a permanent.+--+-- [Validates]+--+--   * Card is in play.+--   * If card belongs to opponent, does not have 'hexproof'.+target :: CardName -> GameMonad ()+target name = do+  actor <- view envActor+  card  <- requireCard name matchInPlay++  let controller = view (cardLocation . _1) card++  unless (actor == controller) $+    validate (missingAttribute hexproof) name++-- | Target a card in a non-play location.+--+-- [Validates]+--+--   * Card is in zone.+targetInLocation :: CardLocation -> CardName -> GameMonad ()+targetInLocation zone = validate (matchLocation zone)+ -- | Activate an ability of a permanent. See 'spendMana' for additional mana -- validations and effects. Typically you will want to `resolve` after -- activating.@@ -458,7 +519,7 @@ -- [Validates] -- --   * Card is in play or graveyard.---   * Card is cotrolled by actor.+--   * Card is controlled by actor. -- -- [Effects] --@@ -485,6 +546,32 @@     stack     ((:) stackName) +-- | Activate a loyalty ability of a planeswalker. Typically you will want to+-- `resolve` after activating.+--+-- > activatePlaneswalker2 "Get a card" (-1) "Karn, Scion of Urza" >> resolveTop+--+-- [Validates]+--+--   * Card is in play.+--   * Card has enough loyalty.+--+-- [Effects]+--+--   * Card loyalty is adjusted.+--+-- See `activate` for additional validations and effects.+activatePlaneswalker :: CardName -> Int -> CardName -> GameMonad ()+activatePlaneswalker stackName loyalty targetName = do+  c <- requireCard targetName matchInPlay++  if view cardLoyalty c + loyalty < 0 then+    throwError $ targetName <> " does not have enough loyalty"+  else+    do+      modifyCard cardLoyalty (+ loyalty) targetName+      activate stackName "" targetName+ -- | Start an attack with the given creatures. -- -- > attackWith ["Fanatical Firebrand"]@@ -494,6 +581,7 @@ --   * Cards are in play. --   * Cards have 'creature' attribute. --   * Cards either have 'haste' or are missing 'summoned'.+--   * Cards do not have 'defender'. -- -- [Effects] --@@ -508,6 +596,7 @@     c <- requireCard cn            (matchInPlay              <> matchAttribute "creature"+             <> missingAttribute "defender"              <> labelMatch "does not have summoning sickness" (                     matchAttribute haste                     `matchOr`@@ -588,6 +677,33 @@        return $ rem - attackPower +-- | Copy a spell on the stack, adding it to top of stack.+--+-- > copySpell "Snap Copy" "Snap"+--+-- [Validates]+--+--   * Card is on stack.+--+-- [Effects]+--+--   * New card is on top of stack.+copySpell newName targetName = do+  card <- requireCard targetName (labelMatch "on stack" $+                      matchLocation (Active, Stack)+            `matchOr` matchLocation (Opponent, Stack)+          )++  let newCard = setAttribute copy . set cardName newName $ card++  modifying+    cards+    (M.insert newName $ BaseCard newCard)++  modifying+    stack+    ((:) newName)+ -- | Applies damage from a source to a target. -- -- > damage (const 2) (targetPlayer Opponent) "Shock"@@ -612,9 +728,9 @@ --     of creatures and planeswalkers. damage ::      (Card -> Int) -- ^ A function that returns the amount of damage to apply,-                   -- given the source card.-  -> Target-  -> CardName+                   --   given the source card.+  -> Target        -- ^ Target to apply damage to+  -> CardName      -- ^ Source card   -> GameMonad () damage f t source = action "damage" $ do   c <- requireCard source mempty@@ -639,15 +755,32 @@       t <- requireCard tn $ matchInPlay <>              (matchAttribute creature `matchOr` matchAttribute planeswalker) +      target tn       when (hasAttribute creature t) $ do-        modifyCard tn cardDamage (+ dmg)+        modifyCardDeprecated tn cardDamage (+ dmg)          when (dmg > 0 && hasAttribute deathtouch c) $           gainAttribute deathtouched tn        when (hasAttribute planeswalker t) $-        modifyCard tn cardLoyalty (\x -> x - dmg)+        modifyCardDeprecated tn cardLoyalty (\x -> x - dmg) ++-- | Destroy a permanent.+--+-- [Validates]+--+--   * Card is in play.+--   * Card is not 'indestructible'+--+-- [Effects]+--+--   * Card is moved to graveyard. See 'move' for possible alternate effects.+destroy :: CardName -> GameMonad ()+destroy targetName = do+  validate (matchInPlay <> missingAttribute indestructible) targetName+  moveTo Graveyard targetName+ -- | Discard a card from the active player's hand. -- -- > discard "Mountain"@@ -673,7 +806,7 @@ -- >       (    matchLocation . view cardLocation -- >         <> const (matchAttribute creature) -- >       )--- >       (pure . over cardStrength (mkStrength (1, 1) <>))+-- >       (pure . over cardStrengthModifier (mkStrength (1, 1) <>)) -- >   $ addCreature (2, 2) "Tah-Crop Elite" -- > attackWith ["Tah-Crop Elite"] -- > exert "Tah-Crop Elite"@@ -690,6 +823,57 @@   validate (matchAttribute tapped) cn   gainAttribute exerted cn +-- | Move a card to the Exile zone.+--+-- > exile "Bridge from Below"+--+-- See `moveTo` for validations and effects.+exile :: CardName -> GameMonad ()+exile = moveTo Exile++-- | Have one card fight another (each deals damage to the other).+--+-- [Validates]+--+--   * Card is in play.+--   * Card is a creature.+--+-- [Effects]+--+--   * Each card has damage dealt to it equal to the other's power. A creature+--     fighting itself will take twice its power in damage.+--   * Note 'runStateBasedActions' handles actual destruction (if applicable)+--     of creatures.+fight :: CardName -> CardName -> GameMonad ()+fight x y = do+  validate (matchInPlay <> matchAttribute creature) x+  validate (matchInPlay <> matchAttribute creature) y++  fight' x y+  fight' y x++  where+    fight' src dst = damage (view cardPower) (targetCard dst) src++-- | Modify the strength of a card in play. It will be reset to base when the+-- card leaves play.+--+-- > modifyStrength (-2, -2) "Soldier"+--+-- [Validates]+--+--   * Card is in play.+--   * Card is a creature.+--+-- [Effects]+--+--   * Changes the strength modifier for the card.+modifyStrength :: (Int, Int) -> CardName -> GameMonad ()+modifyStrength strength cn = do+  _ <- requireCard cn (matchInPlay <> matchAttribute creature)++  modifyCard cardStrengthModifier (mkStrength strength <>) cn+ -- | Move card to location with same controller. -- -- > moveTo Graveyard "Forest"@@ -753,14 +937,23 @@ -- --   * Card is in play. --   * Card is not tapped.+--   * If creature, is not summoned or has haste. -- -- [Effects]: -- --   * Card gains tapped attribute. tap :: CardName -> GameMonad () tap name = do-  validate (matchInPlay <> missingAttribute tapped) name+  c <- requireCard name (matchInPlay <> missingAttribute tapped) +  when (applyMatcher (matchAttribute creature) c) $+    validate (labelMatch "does not have summoning sickness" (+                    matchAttribute haste+                    `matchOr`+                    missingAttribute summoned+             )) name++   gainAttribute tapped name  -- | Untaps a card.@@ -859,10 +1052,11 @@  -- | Pause running of state-based actions for the duration of the action, -- running them at the end.-withStateBasedActions :: GameMonad () -> GameMonad ()+withStateBasedActions :: GameMonad a -> GameMonad a withStateBasedActions m = do-  local (set envSBAEnabled False) m+  x <- local (set envSBAEnabled False) m   runStateBasedActions+  return x  -- | Run state-based actions. These include: --@@ -908,6 +1102,16 @@           when (hasAttribute token c) $             remove cn >> incrementCounter +        let p1 = view cardPlusOneCounters c+        let m1 = view cardMinusOneCounters c+        let p1' = maximum [0, p1 - m1]+        let m1' = maximum [0, m1 - p1]++        when (p1 /= p1' || m1 /= m1') $ do+          modifyCard cardPlusOneCounters (const p1') cn+          modifyCard cardMinusOneCounters (const m1') cn+          incrementCounter+         let matchStack =                        matchLocation (Active, Stack)              `matchOr` matchLocation (Opponent, Stack)@@ -926,14 +1130,30 @@ -- run.  'runStateBasedActions' is implicitly called at the end of each step. -- Nested 'step' invocations execute the nested action but have no other -- effects - generally they should be avoided.-step :: String -> GameMonad () -> GameMonad ()+step :: String -> GameMonad a -> GameMonad a step desc m = withStateBasedActions $ do   b <- get   let (e, b', _) = runMonad b m+  let b'' = over currentStep incrementStep b' -  tell [(desc, b')]-  put b'+  tell [mkStep (view currentStep b'') desc b'']+  put b''    case e of     Left x -> throwError x-    Right _ -> return ()+    Right y -> return y++-- | Branch off a labeled alternative line. Steps inside the fork will be+-- reported at the end of the main line output.+fork :: String -> GameMonad () -> GameMonad ()+fork label m = do+  b <- get+  modifying+    (currentStep . _1)+    (f label)+  m+  put b++  where+    f label Nothing = Just label+    f label (Just existing) = Just $ existing <> " - " <> label
src/Dovin/Attributes.hs view
@@ -28,6 +28,8 @@ deathtouch = "deathtouch" deathtouched :: CardAttribute deathtouched = "deathtouched"+defender :: CardAttribute+defender = "defender" doublestrike :: CardAttribute doublestrike = "doublestrike" enchantment :: CardAttribute
src/Dovin/Builder.hs view
@@ -27,6 +27,7 @@   , withEffect   , withLocation   , withPlusOneCounters+  , withMinusOneCounters   ) where  import Control.Monad.Reader (ask, local)@@ -118,3 +119,7 @@ -- | Set the number of +1/+1 counters of the created card. withPlusOneCounters :: Int -> GameMonad () -> GameMonad () withPlusOneCounters = local . set (envTemplate . cardPlusOneCounters)++-- | Set the number of -1/-1 counters of the created card.+withMinusOneCounters :: Int -> GameMonad () -> GameMonad ()+withMinusOneCounters = local . set (envTemplate . cardMinusOneCounters)
src/Dovin/Dump.hs view
@@ -9,6 +9,10 @@ import qualified Data.HashMap.Strict as M import qualified Data.Set as S import System.Exit+import Data.List (groupBy, sort, sortBy)+import Data.Ord (comparing)+import Data.Function (on)+import Debug.Trace  import Dovin.Actions import Dovin.Attributes@@ -18,94 +22,6 @@ import Dovin.Monad import Dovin.Types -whenMatch :: CardName -> CardMatcher -> GameMonad () -> GameMonad ()-whenMatch name f action = do-  match <- requireCard name f >> pure True `catchError` const (pure False)--  when match action---- ACTIONS------ These correspond to things you can do in Magic. They progress the state--- machine while verifying applicable properties. They all run inside the--- library monad.--target targetName = do-  card <- requireCard targetName (matchInPlay <> missingAttribute hexproof)--  return ()--targetInLocation zone targetName = do-  card <- requireCard targetName (matchLocation zone)--  return ()--destroy targetName = do-  _ <- requireCard targetName (matchInPlay <> missingAttribute indestructible)--  removeFromPlay targetName--sacrifice cn = do-  actor <- view envActor--  validate (matchController actor) cn--  removeFromPlay cn--removeFromPlay cardName = do-  card <- requireCard cardName matchInPlay--  moveTo Graveyard cardName--exile cardName = do-  card <- requireCard cardName mempty--  if hasAttribute "token" card then-    remove cardName-  else-    let loc = view location card in-      move loc (second (const Exile) loc) cardName--copySpell targetName newName = do-  card <- requireCard targetName mempty--  let newCard = setAttribute copy . set cardName newName $ card--  modifying-    cards-    (M.insert newName $ BaseCard newCard)--  modifying-    stack-    ((:) newName)--triggerStorm :: (Int -> GameMonad ()) -> GameMonad ()-triggerStorm action = do-  maybeStorm <- use $ counters . at "storm"--  case maybeStorm of-    Nothing -> throwError "No counter in state: storm"-    Just c -> forM [1..c-1] $ \n -> action n--  return ()--resetStrength :: CardName -> (Int, Int) -> GameMonad ()-resetStrength cn desired = do-  c <- requireCard cn (matchAttribute "creature")--  modifyCard cn cardStrength (const $ mkStrength desired)--modifyStrength :: (Int, Int) -> CardName -> GameMonad ()-modifyStrength (x, y) cn = do-  _ <- requireCard cn (matchInPlay <> matchAttribute "creature")--  modifyCard cn cardStrength (CardStrength x y <>)--  -- Fetch card again to get new strength-  c <- requireCard cn mempty--  when (view cardToughness c <= 0) $ removeFromPlay cn- -- TODO: Better name (resolveMentor?), check source has mentor attribute triggerMentor sourceName targetName = do   source <- requireCard sourceName $ matchAttribute attacking@@ -115,33 +31,6 @@    modifyStrength (1, 1) targetName --fight :: CardName -> CardName -> GameMonad ()-fight x y = do-  cx <- requireCard x (matchInPlay <> matchAttribute creature)-  cy <- requireCard y (matchInPlay <> matchAttribute creature)--  target x-  target y--  fight' cx cy-  unless (cx == cy) $ fight' cy cx--  where-    fight' cx cy = do--      let xdmg = max 0 $ view cardPower cx-      modifyCard (view cardName cy) cardDamage (+ xdmg)-      cy' <- requireCard (view cardName cy) mempty--      when (hasAttribute "lifelink" cx) $-        do-          let owner = fst . view location $ cx-          modifying (life . at owner . non 0) (+ xdmg)--      when (view cardDamage cy' >= view cardToughness cy' || (xdmg > 0 && hasAttribute "deathtouch" cx )) $-        destroy (view cardName cy)- gainLife :: Player -> Int -> GameMonad () gainLife player amount =   modifying@@ -154,50 +43,35 @@ setLife :: Player -> Int -> GameMonad () setLife p n = assign (life . at p) (Just n) -returnToHand cn = do-  actor <- view envActor-  move (actor, Graveyard) (actor, Hand) cn--returnToPlay cn = do-  actor <- view envActor-  move (actor, Graveyard) (actor, Play) cn--activatePlaneswalker :: Int -> CardName -> GameMonad ()-activatePlaneswalker loyalty cn = do-  c <- requireCard cn matchInPlay-  actor <- view envActor--  validate (matchController actor) cn--  if view cardLoyalty c - loyalty < 0 then-    throwError $ cn <> " does not have enough loyalty"-  else-    modifyCard cn cardLoyalty (+ loyalty)----- HIGH LEVEL FUNCTIONS---- fork :: [GameMonad ()] -> GameMonad () fork options = do   b <- get+  let cs = view currentStep b    forM_ options $ \m -> do     m-    put b---with x f = f x+    put $ set currentStep cs b -run :: (Int -> Formatter) -> GameMonad () -> IO ()+run :: (Step -> Formatter) -> GameMonad () -> IO () run formatter solution = do   let (e, _, log) = runMonad emptyBoard solution -  forM_ (zip log [1..]) $ \((step, board), n) -> do-    putStr $ show n <> ". "-    putStr step-    putStrLn (formatter n board)+  let groupedSteps =+        groupBy ((==) `on` view stepFork) . sortBy (comparing $ view stepId ) $ log +  forM_ groupedSteps $ \steps -> do+    case view stepFork $ head steps of+      Just l -> do+        putStrLn ""+        putStrLn $ "=== ALTERNATIVE: " <> l+        putStrLn ""+      Nothing -> return ()++    forM_ steps $ \step -> do+      putStr $ show (view stepNumber step) <> ". "+      putStr $ view stepLabel step+      putStrLn . formatter step . view stepState $ step+   putStrLn ""   case e of     Left x -> do@@ -206,6 +80,3 @@       putStrLn ""       System.Exit.exitFailure     Right _ -> return ()--runVerbose :: GameMonad () -> IO ()-runVerbose = run (const boardFormatter)
src/Dovin/Formatting.hs view
@@ -64,8 +64,13 @@          <> "/"          <> show (view cardToughness c)          <> (let n = view cardPlusOneCounters c in-               if n > 0 then-                  ", +" <> show n <> "/+" <> show n+              if n > 0 then+                ", +" <> show n <> "/+" <> show n+              else+                mempty)+         <> (let n = view cardMinusOneCounters c in+              if n > 0 then+                ", -" <> show n <> "/-" <> show n               else                 mempty)          <> (if view cardDamage c > 0 then
src/Dovin/Helpers.hs view
@@ -3,6 +3,7 @@ module Dovin.Helpers where  import Dovin.Types+import Dovin.Attributes import Dovin.Prelude  import Data.List (sort)@@ -71,12 +72,19 @@    card' <- foldM (\c (e, _) -> applyEffect2 c e) card applicableEffects -  let counterModifier = mconcat-                          . replicate (view cardPlusOneCounters card')-                          . mkStrength $ (1, 1)+  let plusModifier = let n = view cardPlusOneCounters card' in+                          mkStrength (n, n)+  let minusModifier = let n = view cardMinusOneCounters card' in+                          mkStrength (-n, -n) -  return $ over cardStrength (counterModifier <>) card'+  let strengthModifier = view cardStrengthModifier card' +  return+    $ over+        cardStrength+        ((strengthModifier <> plusModifier <> minusModifier) <>)+        card'+   where     applyEffect2 :: Card -> CardEffect -> GameMonad Card     applyEffect2 card e = view effectAction e card@@ -90,12 +98,22 @@    mapM applyEffects bases -modifyCard :: CardName -> ASetter Card Card a b -> (a -> b) -> GameMonad ()-modifyCard name lens f =+modifyCardDeprecated :: CardName -> ASetter Card Card a b -> (a -> b) -> GameMonad ()+modifyCardDeprecated name lens f = do   modifying     (cards . at name . _Just)     (\(BaseCard c) -> BaseCard $ over lens f c) +  card <- requireCard name mempty++  -- This isn't a SBA, it needs to be post-condition here to make sure no funny+  -- business is happening.+  when (view cardPlusOneCounters card < 0) $+    throwError "Cannot reduce +1/+1 counters to less than 0"++modifyCard :: ASetter Card Card a b -> (a -> b) -> CardName -> GameMonad ()+modifyCard lens f name = modifyCardDeprecated name lens f+ -- CARD MATCHERS -- -- Matchers are used for both filtering sets of cards, and also for verifying@@ -115,6 +133,10 @@ matchPlusOneCounters n = CardMatcher (show n <> " +1/+1 counters") $   (==) n . view cardPlusOneCounters +matchMinusOneCounters :: Int -> CardMatcher+matchMinusOneCounters n = CardMatcher (show n <> " -1/-1 counters") $+  (==) n . view cardMinusOneCounters+ matchLocation :: CardLocation -> CardMatcher matchLocation loc = CardMatcher ("in location " <> show loc) $   (==) loc . view cardLocation@@ -140,6 +162,10 @@ matchLesserPower n = CardMatcher ("power < " <> show n) $   (< n) . view cardPower +matchToughness :: Int -> CardMatcher+matchToughness n = labelMatch ("toughness = " <> show n) $ (CardMatcher "" $+  (== n) . view cardToughness) <> matchAttribute creature+ missingAttribute = invert . matchAttribute  (CardMatcher d1 f) `matchOr` (CardMatcher d2 g) =@@ -159,7 +185,7 @@ loseAttribute attr cn = do   c <- requireCard cn mempty -  modifyCard cn id (removeAttribute attr)+  modifyCardDeprecated cn id (removeAttribute attr)  removeAttribute :: CardAttribute -> Card -> Card removeAttribute attr = over cardAttributes (S.delete attr)@@ -167,7 +193,7 @@ gainAttribute attr cn = do   c <- requireCard cn mempty -  modifyCard cn id (setAttribute attr)+  modifyCardDeprecated cn id (setAttribute attr)  setAttribute :: CardAttribute -> Card -> Card setAttribute attr = over cardAttributes (S.insert attr)@@ -180,3 +206,10 @@    forM_ (map (view cardName) matchingCs) f +gameFinished :: GameMonad Bool+gameFinished = do+  state <- use phase++  return $ case state of+             Won _ -> True+             _     -> False
src/Dovin/Monad.hs view
@@ -8,7 +8,7 @@  import Dovin.Types -runMonad :: Board -> GameMonad () -> (Either String (), Board, [(String, Board)])+runMonad :: Board -> GameMonad a -> (Either String a, Board, [Step]) runMonad state m =   let ((e, b), log) = runIdentity $                         runWriterT (runStateT (runReaderT (runExceptT m) emptyEnv) state) in
src/Dovin/Types.hs view
@@ -3,7 +3,7 @@  module Dovin.Types where -import Control.Lens (Lens', Prism', makeLenses, over, view, _1, _Just, at, non)+import Control.Lens (Lens', Prism', makeLenses, over, view, _1, _2, _Just, at, non) import Control.Monad.Reader (ReaderT) import Control.Monad.Except (ExceptT) import Control.Monad.Identity (Identity)@@ -21,7 +21,7 @@ type ManaPool = String type ManaString = String -- TODO: Stack shouldn't be in here because there is only one of them-data Location = Hand | Graveyard | Play | Stack | Exile+data Location = Hand | Graveyard | Play | Stack | Exile | Deck   deriving (Show, Eq, Ord)  data CardEffect = CardEffect@@ -70,6 +70,7 @@   | EndCombat   | SecondMain   | EndStep+  | Won Player   deriving (Show, Eq, Ord)  data Card = Card@@ -78,12 +79,14 @@   , _cardDefaultAttributes :: CardAttributes   , _cardAttributes :: CardAttributes   , _cardStrength :: CardStrength+  , _cardStrengthModifier :: CardStrength   , _cardDamage :: Int   , _cardLoyalty :: Int   , _cardEffects :: [CardEffect]    -- Can probably generalize this more at some point.   , _cardPlusOneCounters :: Int+  , _cardMinusOneCounters :: Int   } instance Hashable Player instance Show Card where@@ -103,12 +106,16 @@   -- deck and graveyard need to be ordered also - but works for now. Need to   -- think more about "hiding" this data structure.   , _stack :: [CardName]+  -- ... well I guess the deck cares about order too now hmmm. Need to figure+  -- this out.+  , _deck :: M.HashMap Player [CardName]   , _counters :: M.HashMap String Int   -- In theory, life could be just another counter. Need to think more about   -- making that happen.   , _life :: M.HashMap Player Int   , _manaPool :: M.HashMap Player ManaPool   , _phase :: Phase+  , _currentStep :: StepIdentifier   }  data Env = Env@@ -117,14 +124,38 @@   , _envActor :: Player   } -type GameMonad a = (ExceptT String (ReaderT Env (StateT Board (WriterT [(String, Board)] Identity)))) a+type StepIdentifier = (Maybe String, Int)+data Step = Step+  { _stepId :: StepIdentifier+  , _stepLabel :: String+  , _stepState :: Board+  }++type GameMonad a+   = (ExceptT String (ReaderT Env (StateT Board (WriterT [Step] Identity)))) a type Formatter = Board -> String +incrementStep :: StepIdentifier -> StepIdentifier+incrementStep (f, s) = (f, s + 1)+ makeLenses ''Board makeLenses ''Card makeLenses ''CardEffect makeLenses ''Env+makeLenses ''Step +stepFork :: Control.Lens.Lens' Step (Maybe String)+stepFork = stepId . _1++stepNumber :: Control.Lens.Lens' Step Int+stepNumber = stepId . _2++mkStep id label state = Step+  { _stepId = id+  , _stepLabel = label+  , _stepState = state+  }+ cardLocation :: Control.Lens.Lens' Card (Player, Location) cardLocation = location @@ -158,7 +189,7 @@ _manaPoolForTyping = view (manaPoolFor Active)  instance Show CardMatcher where-  show _ = "<matcher>"+  show (CardMatcher l _) = l  instance Semigroup CardMatcher where   (CardMatcher d1 f) <> (CardMatcher d2 g) =@@ -189,10 +220,12 @@     , _cardDefaultAttributes = mempty     , _cardAttributes = mempty     , _cardStrength = mempty+    , _cardStrengthModifier = mempty     , _cardDamage = 0     , _cardLoyalty = 0     , _cardEffects = mempty     , _cardPlusOneCounters = 0+    , _cardMinusOneCounters = 0     }  opposing :: Player -> Player@@ -203,8 +236,10 @@                { _cards = mempty                , _counters = mempty                , _stack = mempty+               , _deck = mempty                , _life = mempty                , _manaPool = mempty                , _phase = FirstMain+               , _currentStep = (Nothing, 0)                } 
src/Dovin/V1.hs view
@@ -14,7 +14,7 @@   ) where  import Dovin.Dump-import Dovin.Actions hiding (validate, validateLife, activate, trigger)+import Dovin.Actions hiding (validate, validateLife, activate, trigger, fork) import qualified Dovin.Actions import Dovin.Attributes import Dovin.Builder hiding (withLocation)
src/Dovin/V2.hs view
@@ -1,10 +1,15 @@--- V2 makes the following changes from V1:------   * withLocation now only takes a Location, using the current actor for---     player.---   * Flips argument order for `validate` functions to be consistent with rest---     of API.---   * `activate` and `trigger` use the stack.+{-|+V2 makes the following changes from V1:++* withLocation now only takes a Location, using the current actor for+  player.+* Flips argument order for `validate` functions to be consistent with rest+  of API.+* `activate` and `trigger` use the stack.+* Fork has a saner API and reports properly.+* `formatter` now takes a 'Step' rather than an 'Int'. 'view' is re-exported+  since it will virtually always be required for normal formatters.+-} module Dovin.V2   ( module Dovin.Dump   , module Dovin.Actions@@ -13,17 +18,18 @@   , module Dovin.Formatting   , module Dovin.Helpers   , module Dovin.Types+  , view   )   where  import Dovin.Actions import qualified Dovin.V1 import Dovin.Prelude-import Dovin.Dump hiding (activate, trigger)+import Dovin.Dump hiding (activate, trigger, fork) import Dovin.Attributes import Dovin.Builder import Dovin.Formatting import Dovin.Helpers import Dovin.Monad import Dovin.Types-import Control.Monad.Reader (local)+import Control.Lens (view)
− src/Lib.hs
@@ -1,6 +0,0 @@-module Lib-    ( someFunc-    ) where--someFunc :: IO ()-someFunc = putStrLn "someFunc"
src/Solutions.hs view
@@ -1,4 +1,7 @@ module Solutions where+import Control.Lens (view)+import Dovin.Types (stepNumber)+ import qualified Solutions.ChannelFireball import qualified Solutions.Core19_9 import qualified Solutions.Dominaria5@@ -9,20 +12,32 @@ import qualified Solutions.GuildsOfRavnica8 import qualified Solutions.GuildsOfRavnica9 import qualified Solutions.GuildsOfRavnicaPre2+import qualified Solutions.RavnicaAllegiance3+import qualified Solutions.RavnicaAllegiance4+import qualified Solutions.RavnicaAllegiance5+import qualified Solutions.RavnicaAllegiance7+import qualified Solutions.RavnicaAllegiancePre2 import qualified Solutions.RivalsOfIxalan7 import qualified Solutions.UltimateMasters+import qualified Solutions.WarOfTheSpark2  all = [   ("ChannelFireball", Solutions.ChannelFireball.solution, Solutions.ChannelFireball.formatter),-  ("Core19_9", Solutions.Core19_9.solution, Solutions.Core19_9.formatter),-  ("Dominaria5", Solutions.Dominaria5.solution, Solutions.Dominaria5.formatter),+  ("Core19_9", Solutions.Core19_9.solution, Solutions.Core19_9.formatter . view stepNumber),+  ("Dominaria5", Solutions.Dominaria5.solution, Solutions.Dominaria5.formatter . view stepNumber),   ("Example", Solutions.Example.solution, Solutions.Example.formatter),-  ("ExplorersOfIxalanContest", Solutions.ExplorersOfIxalanContest.solution, Solutions.ExplorersOfIxalanContest.formatter),-  ("GuildsOfRavnica1", Solutions.GuildsOfRavnica1.solution, Solutions.GuildsOfRavnica1.formatter),-  ("GuildsOfRavnica3", Solutions.GuildsOfRavnica3.solution, Solutions.GuildsOfRavnica3.formatter),-  ("GuildsOfRavnica8", Solutions.GuildsOfRavnica8.solution, Solutions.GuildsOfRavnica8.formatter),-  ("GuildsOfRavnica9", Solutions.GuildsOfRavnica9.solution, Solutions.GuildsOfRavnica9.formatter),-  ("GuildsOfRavnicaPre2", Solutions.GuildsOfRavnicaPre2.solution, Solutions.GuildsOfRavnicaPre2.formatter),-  ("RivalsOfIxalan7", Solutions.RivalsOfIxalan7.solution, Solutions.RivalsOfIxalan7.formatter),-  ("UltimateMasters", Solutions.UltimateMasters.solution, Solutions.UltimateMasters.formatter)+  ("ExplorersOfIxalanContest", Solutions.ExplorersOfIxalanContest.solution, Solutions.ExplorersOfIxalanContest.formatter . view stepNumber),+  ("GuildsOfRavnica1", Solutions.GuildsOfRavnica1.solution, Solutions.GuildsOfRavnica1.formatter . view stepNumber),+  ("GuildsOfRavnica3", Solutions.GuildsOfRavnica3.solution, Solutions.GuildsOfRavnica3.formatter . view stepNumber),+  ("GuildsOfRavnica8", Solutions.GuildsOfRavnica8.solution, Solutions.GuildsOfRavnica8.formatter . view stepNumber),+  ("GuildsOfRavnica9", Solutions.GuildsOfRavnica9.solution, Solutions.GuildsOfRavnica9.formatter . view stepNumber),+  ("GuildsOfRavnicaPre2", Solutions.GuildsOfRavnicaPre2.solution, Solutions.GuildsOfRavnicaPre2.formatter . view stepNumber),+  ("RavnicaAllegiance3", Solutions.RavnicaAllegiance3.solution, Solutions.RavnicaAllegiance3.formatter),+  ("RavnicaAllegiance4", Solutions.RavnicaAllegiance4.solution, Solutions.RavnicaAllegiance4.formatter),+  ("RavnicaAllegiance5", Solutions.RavnicaAllegiance5.solution, Solutions.RavnicaAllegiance5.formatter),+  ("RavnicaAllegiance7", Solutions.RavnicaAllegiance7.solution, Solutions.RavnicaAllegiance7.formatter),+  ("RavnicaAllegiancePre2", Solutions.RavnicaAllegiancePre2.solution, Solutions.RavnicaAllegiancePre2.formatter),+  ("RivalsOfIxalan7", Solutions.RivalsOfIxalan7.solution, Solutions.RivalsOfIxalan7.formatter . view stepNumber),+  ("UltimateMasters", Solutions.UltimateMasters.solution, Solutions.UltimateMasters.formatter),+  ("WarOfTheSpark2", Solutions.WarOfTheSpark2.solution, Solutions.WarOfTheSpark2.formatter)   ]
src/Solutions/ChannelFireball.hs view
@@ -73,7 +73,7 @@     withTriggers (cast "1U") "Snap"     target "Beamsplitter Mage"     trigger "Copied Snap" "Beamsplitter Mage" >> resolveTop-    copySpell "Snap" "Snap Copy (Snapcaster Mage)"+    copySpell "Snap Copy (Snapcaster Mage)" "Snap"     target "Snapcaster Mage"    step "Counter Walking Ballista with Rewind, untapping Steam Vents" $ do@@ -127,7 +127,7 @@     withTriggers (flashbackSnapped "1U") "Snap"     target "Beamsplitter Mage"     trigger "Copied Snap" "Beamsplitter Mage" >> resolveTop-    copySpell "Snap" "Snap Copy (Snapcaster Mage)"+    copySpell "Snap Copy (Snapcaster Mage)" "Snap"     target "Snapcaster Mage"    step "Resolve Snap copy, returning Snapcaster, untap Breeding Pools" $ do@@ -269,49 +269,49 @@  playExLandFormatter = cardFormatter "Play (ex. Land)"   (matchLocation (Active, Play) <> invert (matchAttribute land))-formatter 1 = attributes <> boardFormatter-formatter 3 = attributes-  <> cardFormatter "Play" (matchLocation (Active, Play))-  <> cardFormatter "Graveyard" (matchLocation (Active, Graveyard))-formatter 6 = attributes <> stackFormatter-formatter 7 = attributes <> stackFormatter-formatter 8 = attributes <> stackFormatter-  <> cardFormatter "Play" (matchLocation (Active, Play))-formatter 9 = attributes <> stackFormatter-formatter 10 = attributes <> stackFormatter-  <> cardFormatter "Hand" (matchLocation (Active, Hand))-  <> cardFormatter "Graveyard" (matchLocation (Active, Graveyard))-formatter 11 = attributes-  <> cardFormatter "Play" (matchLocation (Active, Play))-  <> cardFormatter "Graveyard" (matchLocation (Active, Graveyard))-formatter 13 = attributes <> stackFormatter-formatter 14 = attributes <> stackFormatter-  <> cardFormatter "Hand" (matchLocation (Active, Hand))-  <> cardFormatter "Graveyard" (matchLocation (Active, Graveyard))-formatter 15 = attributes <> stackFormatter-  <> cardFormatter "Graveyard" (matchLocation (Active, Graveyard))-formatter 16 = attributes-  <> cardFormatter "Hand" (matchLocation (Active, Hand))-  <> cardFormatter "Play" (matchLocation (Active, Play))-formatter 17 = attributes-  <> cardFormatter "Hand" (matchLocation (Active, Hand))-  <> playExLandFormatter-formatter 18 = attributes-  <> playExLandFormatter-  <> cardFormatter "Graveyard" (matchLocation (Active, Graveyard))-formatter 19 = attributes-  <> cardFormatter "Hand" (matchLocation (Active, Hand))-  <> playExLandFormatter-formatter 21 = attributeFormatter (attribute "opponent" $ countLife Opponent) <> attributes-formatter 22 = attributes-  <> cardFormatter "Hand" (matchLocation (Active, Hand))-  <> playExLandFormatter-  <> cardFormatter "Graveyard" (matchLocation (Active, Graveyard))-formatter 23 = attributes-  <> cardFormatter "Hand" (matchLocation (Active, Hand))-  <> playExLandFormatter-formatter 25 = attributeFormatter (attribute "opponent" $ countLife Opponent) <> attributes-formatter _ = attributes+formatter step = attributes <> case view stepNumber step of+  1 -> boardFormatter+  3 -> cardFormatter "Play" (matchLocation (Active, Play))+    <> cardFormatter "Graveyard" (matchLocation (Active, Graveyard))+  6 -> stackFormatter+  7 -> stackFormatter+  8 -> stackFormatter+    <> cardFormatter "Play" (matchLocation (Active, Play))+  9 -> stackFormatter+  10 -> stackFormatter+    <> cardFormatter "Hand" (matchLocation (Active, Hand))+    <> cardFormatter "Graveyard" (matchLocation (Active, Graveyard))+  11  ->+       cardFormatter "Play" (matchLocation (Active, Play))+    <> cardFormatter "Graveyard" (matchLocation (Active, Graveyard))+  13 -> stackFormatter+  14 -> stackFormatter+    <> cardFormatter "Hand" (matchLocation (Active, Hand))+    <> cardFormatter "Graveyard" (matchLocation (Active, Graveyard))+  15 -> stackFormatter+    <> cardFormatter "Graveyard" (matchLocation (Active, Graveyard))+  16 ->+       cardFormatter "Hand" (matchLocation (Active, Hand))+    <> cardFormatter "Play" (matchLocation (Active, Play))+  17 ->+       cardFormatter "Hand" (matchLocation (Active, Hand))+    <> playExLandFormatter+  18 ->+       playExLandFormatter+    <> cardFormatter "Graveyard" (matchLocation (Active, Graveyard))+  19 ->+       cardFormatter "Hand" (matchLocation (Active, Hand))+    <> playExLandFormatter+  21 -> attributeFormatter (attribute "opponent" $ countLife Opponent)+  22 ->+       cardFormatter "Hand" (matchLocation (Active, Hand))+    <> playExLandFormatter+    <> cardFormatter "Graveyard" (matchLocation (Active, Graveyard))+  23 ->+      cardFormatter "Hand" (matchLocation (Active, Hand))+    <> playExLandFormatter+  25 -> attributeFormatter (attribute "opponent" $ countLife Opponent)+  _ -> mempty  spellCounter :: Lens' Board Int spellCounter = counters . at "spell-count" . non 0
src/Solutions/Dominaria5.hs view
@@ -30,7 +30,7 @@       addCreature (5, 5) "Bonded Horncrest"    step "Activate Karn to return Mutiny" $ do-    activatePlaneswalker (-1) "Karn, Scion of Urza 1"+    activatePlaneswalker "Get card" (-1) "Karn, Scion of Urza 1" >> resolveTop     moveTo Hand "Mutiny"    step "Cast Mutiny, using Horncrest to destroy a Responder" $ do@@ -49,7 +49,7 @@     moveTo Graveyard "Karn, Scion of Urza 1"    step "Activate Karn to return Fatal Push" $ do-    activatePlaneswalker (-1) "Karn, Scion of Urza 2"+    activatePlaneswalker "Get card" (-1) "Karn, Scion of Urza 2" >> resolveTop     moveTo Hand "Fatal Push"    step "Cast Fatal Push destroying remaining Responder with Revolt from first Karn" $ do
src/Solutions/Example.hs view
@@ -31,10 +31,11 @@       validate (matchAttribute flying) enemy       destroy enemy -formatter :: Int -> Formatter-formatter 2 = manaFormatter-  <> cardFormatter "opponent creatures" (matchLocation (Opponent, Play))-formatter _ = boardFormatter+formatter :: Step -> Formatter+formatter step = case view stepNumber step of+  1 -> manaFormatter+    <> cardFormatter "opponent creatures" (matchLocation (Opponent, Play))+  _ -> boardFormatter  manaFormatter = attributeFormatter $ do   attribute "availble mana" $
src/Solutions/GuildsOfRavnica1.hs view
@@ -35,7 +35,7 @@     resolve "Aurelia, Exemplar of Justice"    step "Angrath gain control, targeting Aurelia" $ do-    activatePlaneswalker (-3) "Angrath, the Flame-Chained"+    activatePlaneswalker "" (-3) "Angrath, the Flame-Chained" >> resolveTop     target "Aurelia, Exemplar of Justice"     gainAttribute "haste" "Aurelia, Exemplar of Justice" 
src/Solutions/GuildsOfRavnica3.hs view
@@ -1,6 +1,7 @@ module Solutions.GuildsOfRavnica3 where  import Control.Monad+import Control.Monad.Except (catchError)  import Dovin.V1 @@ -139,5 +140,11 @@         combatDamage blockers "Undercity Necrolisk"         validateLife Opponent 0     ]++whenMatch :: CardName -> CardMatcher -> GameMonad () -> GameMonad ()+whenMatch name f action = do+  match <- requireCard name f >> pure True `catchError` const (pure False)++  when match action  formatter _ = boardFormatter
src/Solutions/GuildsOfRavnica8.hs view
@@ -79,7 +79,7 @@     tapForMana "B" "Memorial to Folly 1"     withTriggers (cast "B") "March of the Drowned"     resolve "March of the Drowned"-    returnToHand "Vicious Conquistador"+    moveTo Hand "Vicious Conquistador"    step "Vicious Conquistador" $ do     tapForMana "B" "Memorial to Folly 2"@@ -92,7 +92,6 @@     target "Vicious Conquistador"     resolve "Dead Weight"     modifyStrength (-2, -2) "Vicious Conquistador"-    resetStrength "Vicious Conquistador" (1, 2)     moveTo Graveyard "Dead Weight"    step "Gruesome Menagerie for Sailor of Means and Vicious Conquistador" $ do@@ -105,8 +104,8 @@     resolve "Gruesome Menagerie"     targetInLocation (Active, Graveyard) "Vicious Conquistador"     targetInLocation (Active, Graveyard) "Sailor of Means"-    returnToPlay "Vicious Conquistador"-    returnToPlay "Sailor of Means"+    moveTo Play "Vicious Conquistador"+    moveTo Play "Sailor of Means"     withLocation (Active, Play)       $ withAttribute token       $ addArtifact "Treasure"@@ -116,7 +115,6 @@     withTriggers (castWithMuldrotha "enchantment" "B") "Dead Weight"     target "Vicious Conquistador"     modifyStrength (-2, -2) "Vicious Conquistador"-    resetStrength "Vicious Conquistador" (1, 2)     moveTo Graveyard "Dead Weight"    step "Find for Vicous Conquistador" $ do@@ -125,7 +123,7 @@     withTriggers (cast "BB") "Find"     resolve "Find"     targetInLocation (Active, Graveyard) "Vicious Conquistador"-    returnToHand "Vicious Conquistador"+    moveTo Hand "Vicious Conquistador"    step "Vicious Conquistador" $ do     tapForMana "B" "Treasure"
src/Solutions/GuildsOfRavnica9.hs view
@@ -2,6 +2,7 @@  import Control.Lens import Control.Monad+import Control.Monad.Except (throwError)  import Dovin.V1 @@ -16,8 +17,9 @@         action name         trigger "Thousand-Year Storm"         triggerStorm $-          \n -> copySpell name+          \n -> copySpell                   (numbered (n + 1) (zipWith const name (drop 2 name)))+                  name          trigger "Adeliz, the Cinder Wind"         modifyStrength (1, 1) "Adeliz, the Cinder Wind"@@ -97,7 +99,8 @@       (gainAttribute deathtouch)      with "Shalai, Voice of Plenty" $ \enemy -> do-      fight "Adeliz, the Cinder Wind" enemy+      withStateBasedActions $+        fight "Adeliz, the Cinder Wind" enemy       validate enemy $ matchLocation (Opponent, Graveyard)    step "Cast Doublecast" $ do@@ -113,8 +116,8 @@     withTriggers (cast "G") "Plummet 1"      -- From double doublecast earlier-    copySpell "Plummet 1" "Plummet 4"-    copySpell "Plummet 1" "Plummet 5"+    copySpell "Plummet 4" "Plummet 1"+    copySpell "Plummet 5" "Plummet 1"      resolve "Plummet 5"     with "Lyra Dawnbringer" $ \enemy -> do@@ -138,7 +141,7 @@     withTriggers (cast "UU") "Quasiduplicate 1"      with ("Merfolk Mistbinder 2") $ \enemy -> do-      forM_ [1..4] $ \n -> do+      withStateBasedActions $ forM_ [1..4] $ \n -> do         let tokenName = ("Afzocan Archer " <> show n)         resolve $ numbered (5 - n) "Quasiduplicate"         addArcherCopy tokenName@@ -152,7 +155,7 @@     withTriggers (jumpstart "UU" "Waterknot") "Quasiduplicate 1"      with (numbered 1 "Merfolk Mistbinder") $ \enemy -> do-      forM_ [1..2] $ \n -> do+      withStateBasedActions $ forM_ [1..2] $ \n -> do         let tokenName = ("Afzocan Archer " <> show n)         resolve $ numbered (6 - n) "Quasiduplicate"         addArcherCopy tokenName@@ -161,7 +164,7 @@       validate enemy $ matchLocation (Opponent, Graveyard)      with "Kopala, Warden of Waves" $ \enemy -> do-      forM_ [3..4] $ \n -> do+      withStateBasedActions $ forM_ [3..4] $ \n -> do         let tokenName = numbered n "Afzocan Archer"         resolve $ numbered (6 - n) "Quasiduplicate"         addArcherCopy tokenName@@ -200,3 +203,14 @@   attribute "adeliz" $     view cardStrength <$> requireCard "Adeliz, the Cinder Wind" mempty   attribute "enemies" $ countCards (matchLocation (Opponent, Play))++triggerStorm :: (Int -> GameMonad ()) -> GameMonad ()+triggerStorm action = do+  maybeStorm <- use $ counters . at "storm"++  case maybeStorm of+    Nothing -> throwError "No counter in state: storm"+    Just c -> forM [1..c-1] $ \n -> action n++  return ()+
src/Solutions/GuildsOfRavnicaPre2.hs view
@@ -43,7 +43,7 @@     sacrifice "Whisper Agent"     sacrifice "Whisper, Blood Liturgist" -    returnToPlay "Torch Courier"+    moveTo Play "Torch Courier"    step "Play Rhizome Lurcher, haste with Torch Courier" $ do     tapForMana "G" "Woodland Cemetery 3"
+ src/Solutions/RavnicaAllegiance3.hs view
@@ -0,0 +1,128 @@+module Solutions.RavnicaAllegiance3 where++import Dovin.V2+import Dovin.Prelude++solution :: GameMonad ()+solution = do+  step "Initial Setup" $ do+    setLife Opponent 15++    withLocation Play $ do+      addLands 2 "Godless Shrine"+      addLands 2 "Stomping Ground"+      addCreature (1, 1) "Elenda, the Dusk Rose"+      withAttribute legendary+        $ withEffect+           matchInPlay+           (matchOtherCreatures <> const (matchAttribute token))+           (pure . setAttribute vigilance . setAttribute lifelink)+        $ addCreature (2, 4) "Teysa Karlov"+      addCreature (2, 2) "Pitiless Pontiff"+      withAttribute defender $ addCreature (2, 3) "Novice Knight"+      withAttribute legendary+        $ withEffect+            matchInPlay+            matchOtherCreatures+            (pure . over cardStrength (mkStrength (1, 0) <>))+        $ addCreature (2, 2) "Judith, the Scourge Diva"++    withLocation Hand $ do+      withAttribute haste $ addCreature (3, 4) "Grand Warlord Radha"+      addInstant "Status // Statue"++    as Opponent $ do+      withLocation Play $ do+        -- Note: opponent has hexproof too! Not modeled here.+        withAttributes [flying]+          $ withEffect+              matchInPlay+              matchOtherCreatures+              (pure . setAttribute hexproof)+          $ addCreature (3, 4) "Shalai, Voice of Plenty"++        addCreature (2, 5) "Humongulus"+        addCreature (6, 6) "Rakdos, the Showstopper"++  step "Play Radha" $ do+    tapForMana "R" "Stomping Ground 1"+    tapForMana "G" "Stomping Ground 2"+    tapForMana "W" "Godless Shrine 1"+    tapForMana "W" "Godless Shrine 2"++    cast "2RW" "Grand Warlord Radha" >> resolveTop+  +  step "Attack with all except knight, generating mana" $ do+    attackWith+      [ "Judith, the Scourge Diva"+      , "Pitiless Pontiff"+      , "Teysa Karlov"+      , "Elenda, the Dusk Rose"+      , "Grand Warlord Radha"+      ]++    trigger "Generate Mana" "Grand Warlord Radha" >> resolveTop+    forCards+      (matchAttribute attacking)+      (const $ addMana "G")++  step "Status Judith to give her +1/+1 and deathtouch" $ do+    cast "G" "Status // Statue" >> resolveTop+    with "Judith, the Scourge Diva" $ \cn -> do+      gainAttribute deathtouch cn+      modifyCardDeprecated cn cardPlusOneCounters (+ 1)++  step "Sacrifice Knight to make Pontiff indestructible, ping Shalai and Rakdos with Judith" $ do+    activate "Indestructible" "1" "Pitiless Pontiff"+    sacrifice "Novice Knight"++    trigger "+1 1" "Elenda, the Dusk Rose"+    trigger "+1 2" "Elenda, the Dusk Rose"+    trigger "Judith for Elenda 1" "Judith, the Scourge Diva"+    trigger "Judith for Elenda 2" "Judith, the Scourge Diva"++    -- Need to put Shalai in the bin to remove hexproof from Rakdos+    withStateBasedActions $ do+      resolveTop+      damage (const 1) (targetCard "Shalai, Voice of Plenty") "Judith, the Scourge Diva"++    resolveTop+    damage (const 1) (targetCard "Rakdos, the Showstopper") "Judith, the Scourge Diva"++    resolveTop+    modifyCardDeprecated "Elenda, the Dusk Rose" cardPlusOneCounters (+ 1)+    resolveTop+    modifyCardDeprecated "Elenda, the Dusk Rose" cardPlusOneCounters (+ 1)++    resolve "Indestructible"+    gainAttribute indestructible "Pitiless Pontiff"+    gainAttribute deathtouch "Pitiless Pontiff"+    +  step "Apply combat damage, doesn't matter what Humongulus blocks, can't kill them" $ do+    combatDamage [] "Grand Warlord Radha"+    combatDamage [] "Judith, the Scourge Diva"+    combatDamage [] "Pitiless Pontiff"+    combatDamage [] "Teysa Karlov"+    combatDamage ["Humongulus"] "Elenda, the Dusk Rose"++  step "Sacrifice Radha, ping opponent (ignore Elenda triggers, irrelevant)" $ do+    activate "Indestructible" "1" "Pitiless Pontiff"+    sacrifice "Grand Warlord Radha"++    trigger "Judith for Elenda 1" "Judith, the Scourge Diva"+    trigger "Judith for Elenda 2" "Judith, the Scourge Diva"++    resolveTop+    damage (const 1) (targetPlayer Opponent) "Judith, the Scourge Diva"+    resolveTop+    damage (const 1) (targetPlayer Opponent) "Judith, the Scourge Diva"++    resolve "Indestructible"+    gainAttribute indestructible "Pitiless Pontiff"+    gainAttribute deathtouch "Pitiless Pontiff"++attributes = attributeFormatter $ do+  attribute "life" $ countLife Opponent+  attribute "pool" $ countManaPool Active+    +formatter _ = attributes <> boardFormatter
+ src/Solutions/RavnicaAllegiance4.hs view
@@ -0,0 +1,100 @@+module Solutions.RavnicaAllegiance4 where++import Dovin.V2+import Dovin.Prelude++solution :: GameMonad ()+solution = do+  step "Initial state" $ do+    setLife Opponent 13++    withLocation Play $ do+      addEnchantment "Rhythm of the Wild"+      addCreature (2, 2) "Combine Guildmage"+      addCreature (2, 3) "Herald of Secret Streams"+      addLands 4 "Breeding Pool"+      addLands 4 "Stomping Ground"++    withLocation Hand $ do+      addCreature (2, 2) "Zhur-Taa Goblin"+      withAttribute trample $ addCreature (3, 3) "Galloping Lizrog"+      addCreature (0, 2) "Incubation Druid"++  step "Activate Guildmage for extra +1/+1 counters" $ do+    tapForMana "G" "Stomping Ground 1"+    tapForMana "G" "Stomping Ground 2"+    tap "Combine Guildmage"+    activate "Extra +1/+1" "1G" "Combine Guildmage"+    resolveTop++  step "Cast Incubation Druid with haste from riot, +1/+1 from guildmage" $ do+    tapForMana "G" "Breeding Pool 1"+    tapForMana "G" "Breeding Pool 2"+    cast "1G" "Incubation Druid" >> resolveTop++    trigger "+1/+1" "Combine Guildmage"+    trigger "Riot" "Rhythm of the Wild"++    resolve "Riot"+    gainAttribute haste "Incubation Druid"++    resolve "+1/+1"+    modifyCardDeprecated "Incubation Druid" cardPlusOneCounters (+ 1)++  step "Cast Zhur-Taa, double Riot and bonus +1/+1 for 3 total" $ do+    tapForMana "G" "Stomping Ground 3"+    tapForMana "R" "Stomping Ground 4"++    cast "RG" "Zhur-Taa Goblin" >> resolveTop++    trigger "+1/+1" "Combine Guildmage"+    trigger "Rhythm Riot" "Rhythm of the Wild"+    trigger "Zhur-Taa Riot" "Zhur-Taa Goblin"++    resolve "Zhur-Taa Riot"+    modifyCardDeprecated "Zhur-Taa Goblin" cardPlusOneCounters (+ 1)++    resolve "Rhythm Riot"+    modifyCardDeprecated "Zhur-Taa Goblin" cardPlusOneCounters (+ 1)++    resolve "+1/+1"+    modifyCardDeprecated "Zhur-Taa Goblin" cardPlusOneCounters (+ 1)++  step "Tap Druid for 3 mana, add to lands to cast Lizrog" $ do+    tap "Incubation Druid" -- TODO: Check if this checks for haste?+    activate "Mana" "" "Incubation Druid" >> resolveTop++    addMana "GGG"+    tapForMana "U" "Breeding Pool 3"+    tapForMana "U" "Breeding Pool 4"++    cast "3UG" "Galloping Lizrog" >> resolveTop+    trigger "+1/+1 swap" "Galloping Lizrog"+    trigger "+1/+1" "Combine Guildmage"+    trigger "Rhythm Riot" "Rhythm of the Wild"++    resolve "Rhythm Riot"+    gainAttribute haste "Galloping Lizrog"++    resolve "+1/+1"+    modifyCardDeprecated "Galloping Lizrog" cardPlusOneCounters (+ 1)++    resolve "+1/+1 swap"+    modifyCardDeprecated "Galloping Lizrog" cardPlusOneCounters (\x -> x - 1)+    modifyCardDeprecated "Zhur-Taa Goblin" cardPlusOneCounters (\x -> x - 3)+    modifyCardDeprecated "Incubation Druid" cardPlusOneCounters (\x -> x - 1)++    modifyCardDeprecated "Galloping Lizrog" cardPlusOneCounters (+ (5 * 2))++  step "Attacking with Lizrog, unblockable from Herald" $ do+    validate matchInPlay "Herald of Secret Streams"++    attackWith ["Galloping Lizrog"]+    combatDamage [] "Galloping Lizrog"++    validateLife 0 Opponent++attributes = attributeFormatter $ do+  attribute "life" $ countLife Opponent++formatter _ = attributes <> boardFormatter
+ src/Solutions/RavnicaAllegiance5.hs view
@@ -0,0 +1,214 @@+module Solutions.RavnicaAllegiance5 where++import Dovin.V2+import Dovin.Prelude++tapWithFreyalise mana card = do+  validate matchInPlay "Song of Freyalise"+  tapForMana mana card++-- Approach:+--   * Song of Freyalise and tapping creatures for mana+--   * Use Depths to clear first creature+--   * Use Wildgrowth Walker to gain life (which untaps Famished Paladin)+--   * Use Vona x 4 to clear opponent's board+--   * Attack with all with +1/+1 from Roar and an explore for lethal+solution :: GameMonad ()+solution = do+  step "Initial state" $ do+    setLife Opponent 16+    setLife Active 17++    withLocation Graveyard $ do+      addLand "Forest"+      addLand "Swamp"++    withLocation Hand $ do+      addEnchantment "Song of Freyalise"+      addCreature (1, 3) "Wildgrowth Walker"+      addCreature (2, 1) "Merfolk Branchwalker"+      addSorcery "Enter the Unknown"+      addInstant "Rallying Roar"+      addInstant "Depths of Desire"++    withLocation Play $ do+      addEnchantment "Fall of the Thran"+      addEnchantment "Path of Discovery"+      addCreature (3, 3) "Famished Paladin"+      withAttribute vigilance $ do+        addCreature (4, 4) "Vona, Butcher of Magan"+        addCreature (2, 3) "Tenth District Veteran"+      addCreature (2, 5) "Cacophodon"+      addPlaneswalker 3 "Jace, Ingenious Mind-Mage"++    as Opponent $ do+      withLocation Play $ do+        addCreature (3, 4) "Temple Altisaur"+        addCreature (2, 2) "Everdawn Champion"+        addCreature (2, 4) "Baird, Steward of Argive"+        addCreature (2, 1) "Dauntless Bodyguard"+        addCreature (3, 5) "Imperial Ceratops"++  step "Return lands from Fall of Thran trigger" $ do+    trigger "" "Fall of the Thran" >> resolveTop+    moveTo Play "Forest"+    moveTo Play "Swamp"++  step "Cast Song of Freyalise" $ do+    tapForMana "G" "Forest"+    tapForMana "B" "Swamp"+    cast "1G" "Song of Freyalise" >> resolveTop++  step "Use Vona to destroy Bodyguard" $ do+    activate "Destroy" "" "Vona, Butcher of Magan"+    tap "Vona, Butcher of Magan"+    loseLife Active 7++    resolveTop++    target "Dauntless Bodyguard"+    destroy "Dauntless Bodyguard"++  step "Cast Depths on Altisaur using creatures" $ do+    forCards (matchController Active <> matchInPlay <> matchAttribute creature <> missingAttribute tapped <> missingAttribute summoned) (tapWithFreyalise "U")++    cast "2U" "Depths of Desire" >> resolveTop++    as Opponent $+      moveTo Hand "Temple Altisaur"+    withLocation Play $ withAttribute token $ addArtifact "Treasure"++  step "Untap with Jace" $ do+    activatePlaneswalker "" 1 "Jace, Ingenious Mind-Mage" >> resolveTop++    forCards (matchController Active <> matchInPlay <> matchAttributes [creature, tapped]) untap++  step "Cast Wildgrowth by tapping creaturs" $ do+    tapWithFreyalise "G" "Famished Paladin"+    tapWithFreyalise "G" "Tenth District Veteran"++    cast "1G" "Wildgrowth Walker" >> resolveTop++    trigger "Explore" "Path of Discovery" >> resolveTop+    modifyCardDeprecated "Wildgrowth Walker" cardPlusOneCounters (+ 1)+    trigger "Gain life" "Wildgrowth Walker" >> resolveTop+    gainLife Active 3+    trigger "Untap" "Famished Paladin" >> resolveTop+    untap "Famished Paladin"++  step "Cast Branchwalker, floating mana from paladin untaps" $ do+    tapWithFreyalise "G" "Famished Paladin"+    tapWithFreyalise "G" "Cacophodon"++    cast "1G" "Merfolk Branchwalker" >> resolveTop++    trigger "Explore Path" "Path of Discovery"+    trigger "Explore Brachwalker" "Merfolk Branchwalker"++    resolveTop+    modifyCardDeprecated "Merfolk Branchwalker" cardPlusOneCounters (+ 1)+    trigger "Gain life" "Wildgrowth Walker" >> resolveTop+    gainLife Active 3+    trigger "Untap" "Famished Paladin" >> resolveTop+    untap "Famished Paladin"++    tapWithFreyalise "G" "Famished Paladin"++    resolveTop+    modifyCardDeprecated "Merfolk Branchwalker" cardPlusOneCounters (+ 1)+    trigger "Gain life" "Wildgrowth Walker" >> resolveTop+    gainLife Active 3+    trigger "Untap" "Famished Paladin" >> resolveTop+    untap "Famished Paladin"++    tapWithFreyalise "G" "Famished Paladin"++  step "Enter the Unknown on Tenth District Veteran" $ do+    cast "G" "Enter the Unknown" >> resolveTop+    target "Tenth District Veteran"++    trigger "Explore Path" "Path of Discovery"++    resolveTop+    modifyCardDeprecated "Tenth District Veteran" cardPlusOneCounters (+ 1)+    trigger "Gain life" "Wildgrowth Walker" >> resolveTop+    gainLife Active 3+    trigger "Untap" "Famished Paladin" >> resolveTop+    untap "Famished Paladin"++  step "Use Vona to destroy Baird" $ do+    activate "Destroy" "" "Vona, Butcher of Magan"+    tap "Vona, Butcher of Magan"+    loseLife Active 7++    resolveTop++    target "Baird, Steward of Argive"+    destroy "Baird, Steward of Argive"++  step "Float mana from creatures and artifact, untap with Rallying Roar" $ do+    forCards (matchController Active <> matchInPlay <> matchAttribute creature <> missingAttribute tapped <> missingAttribute summoned) (tapWithFreyalise "W")++    sacrifice "Treasure"+    addMana "1"++    cast "2W" "Rallying Roar" >> resolveTop++    forCards (matchController Active <> matchInPlay <> matchAttribute creature) (modifyStrength (1, 1))+    forCards (matchController Active <> matchInPlay <> matchAttributes [creature, tapped]) untap++  step "Attack with all" $ do+    attackWith+      [ "Famished Paladin"+      , "Tenth District Veteran"+      , "Vona, Butcher of Magan"+      , "Cacophodon"+      ]++  step "Use Vona to destroy Ceratops in response to Tenth District Veteran trigger" $ do+    trigger "Untap" "Tenth District Veteran"++    activate "Destroy" "" "Vona, Butcher of Magan"+    tap "Vona, Butcher of Magan"+    loseLife Active 7++    resolveTop++    target "Imperial Ceratops"+    destroy "Imperial Ceratops"+++  step "Resolve veteran trigger, untap Vona" $ do+    resolve "Untap"+    untap "Vona, Butcher of Magan"++  step "Use Vona to destroy Everdawn Champion" $ do+    activate "Destroy" "" "Vona, Butcher of Magan"+    tap "Vona, Butcher of Magan"+    loseLife Active 7++    resolveTop++    target "Everdawn Champion"+    destroy "Everdawn Champion"++    validateLife 1 Active++  step "Combat damage for the win" $ do+    forCards (matchAttribute attacking) (combatDamage [])+    validateLife 0 Opponent++attributes = attributeFormatter $ do+  attribute "op. life" $ countLife Opponent+  attribute "our life" $ countLife Active+  attribute "pool" $ countManaPool Active++formatter step = attributes <> case view stepNumber step of+  1  -> boardFormatter+  4  -> cardFormatter "defending" (matchInPlay <> matchController Opponent)+  5  -> cardFormatter "defending" (matchInPlay <> matchController Opponent)+  10 -> cardFormatter "defending" (matchInPlay <> matchController Opponent)+  12 -> cardFormatter "attacking" (matchAttribute attacking)+  13 -> cardFormatter "defending" (matchInPlay <> matchController Opponent)+  15 -> cardFormatter "defending" (matchInPlay <> matchController Opponent)+  _ -> mempty
+ src/Solutions/RavnicaAllegiance7.hs view
@@ -0,0 +1,95 @@+module Solutions.RavnicaAllegiance7 where++import Dovin.V2+import Dovin.Prelude++unblockable = "unblockable"++solution :: GameMonad ()+solution = do+  step "Initial state" $ do+    setLife Opponent 18++    withLocation Hand $ do+      addAura "Deep Freeze 1"+      addAura "Sky Tether"+      addSorcery "Ghostform"++    withLocation Play $ do+      addLands 4 "Hallowed Fountain"+      addLands 2 "Stomping Ground"+      withAttribute trample $ addCreature (0, 8) "Erratic Cyclops"+      withAttribute defender $ addCreature (0, 4) "Suspicious Bookcase 1"+      withAttribute defender $ addCreature (0, 4) "Suspicious Bookcase 2"+      addCreature (2, 1) "Goblin Locksmith"+      withAttributes [flying, vigilance] $ addCreature (3, 5) "Arcades, the Strategist"+      addAura "Deep Freeze 2"++    as Opponent $ do+      withLocation Play $ do+        withAttributes [flying, lifelink] $ addCreature (5, 5) "Lyra Dawnbringer"+        addCreature (1, 1) "Gyre Engineer 1"+        addCreature (1, 1) "Gyre Engineer 2"+        withAttribute defender $ addCreature (0, 4) "Gyre Engineer 3"++  step "Deep Freeze opposing Lyra" $ do+    tapForMana "U" "Hallowed Fountain 1"+    tapForMana "U" "Hallowed Fountain 2"+    tapForMana "G" "Stomping Ground 1"+    cast "2U" "Deep Freeze 1" >> resolveTop++    target "Lyra Dawnbringer"+    gainAttribute defender "Lyra Dawnbringer"++  step "Sky Tether Erratic Cyclops" $ do+    tapForMana "W" "Hallowed Fountain 3"+    cast "W" "Sky Tether" >> resolveTop++    target "Erratic Cyclops"+    gainAttribute defender "Erratic Cyclops"++  step "Ghostform both bookcases" $ do+    tapForMana "U" "Hallowed Fountain 4"+    tapForMana "G" "Stomping Ground 2"++    cast "1U" "Ghostform" >> resolveTop++    forM_ [1, 2] $ \n -> do+      target $ numbered n "Suspicious Bookcase"+      gainAttribute unblockable (numbered n "Suspicious Bookcase")++  step "Attack with all, only two wizards can block Goblin and Cyclops" $ do+    -- Don't use attackWith here, because want to allow defenders to attack+    let attackers = matchInPlay <> matchAttribute creature+    forCards attackers $ \cn -> do+      tap cn+      gainAttribute attacking cn++    forCards (attackers <> matchAttribute defender) $ \cn -> do+      -- Fake Arcades effect, it shouldn't actually change their power.+      modifyCard cardStrength (\(CardStrength p t) -> CardStrength t t) cn++    -- Defenders can't block, per Goblin Locksmith+    validate (matchAttribute defender) "Gyre Engineer 3"+    validate (matchAttribute defender) "Lyra Dawnbringer"++    -- No flyers to block+    validate (matchAttribute flying) "Arcades, the Strategist"+    combatDamage [] "Arcades, the Strategist"++    -- Bookcases cannot be blocked+    validate (matchAttribute unblockable) "Suspicious Bookcase 1"+    combatDamage [] "Suspicious Bookcase 1"+    validate (matchAttribute unblockable) "Suspicious Bookcase 2"+    combatDamage [] "Suspicious Bookcase 2"++    -- Only remaining blocks+    combatDamage ["Gyre Engineer 1"] "Goblin Locksmith"+    combatDamage ["Gyre Engineer 2"] "Erratic Cyclops"++    validateLife 0 Active++attributes = attributeFormatter $ do+  attribute "life" $ countLife Opponent++formatter _ = attributes <> boardFormatter
+ src/Solutions/RavnicaAllegiancePre2.hs view
@@ -0,0 +1,149 @@+module Solutions.RavnicaAllegiancePre2 where++import Dovin.V2+import Dovin.Prelude++import Data.List (delete)+import Control.Lens+import Debug.Trace++solution :: GameMonad ()+solution = do+  step "Initial Setup" $ do+    setLife Opponent 7++    withLocation Play $ do+      addLands 3 "Blood Crypt"+      addLands 3 "Overgrown Tomb"+      withAttribute flying $ addCreature (3, 3) "Mausoleum Harpy"+      withAttribute defender $ addCreature (4, 3) "Piston-Fist Cyclops"+      withAttribute defender $ addCreature (0, 2) "Dragon Egg"+      addEnchantment "Rhythm of the Wild 1"+      addEnchantment "Rhythm of the Wild 2"++    withLocation Hand $ do+      addInstant "Ancient Animus"+      addCreature (2, 2) "Growth-Chamber Guardian 1"++    withLocation Deck $ do+      addCreature (2, 2) "Growth-Chamber Guardian 2"++    as Opponent $ do+      withLocation Play $ do+        withAttribute doublestrike $ addCreature (3, 3) "Goring Ceratops"+        withAttribute firststrike $ addCreature (3, 3) "Goblin Chainwhirler 1"+        withAttribute firststrike $ addCreature (3, 3) "Goblin Chainwhirler 2"++  step "Cast Guardian, get haste/+1 from Rhythm triggers" $ do+    tapForMana "G" "Overgrown Tomb 1"+    tapForMana "R" "Blood Crypt 1"+    cast "1G" "Growth-Chamber Guardian 1" >> resolveTop++    trigger "Haste" "Rhythm of the Wild 1"+    trigger "+1/+1" "Rhythm of the Wild 2"+    resolveTop+    modifyCardDeprecated "Growth-Chamber Guardian 1" cardPlusOneCounters (+ 1)+    trigger "Search" "Growth-Chamber Guardian 1"++    resolveTop+    move (Active, Deck) (Active, Hand) "Growth-Chamber Guardian 2"++    resolveTop+    gainAttribute haste "Growth-Chamber Guardian 1"++  step "Cast second Guardian, same haste/+1 combo (decline search)" $ do+    tapForMana "G" "Overgrown Tomb 2"+    tapForMana "R" "Blood Crypt 2"+    cast "1G" "Growth-Chamber Guardian 2" >> resolveTop++    trigger "Haste" "Rhythm of the Wild 1"+    trigger "+1/+1" "Rhythm of the Wild 2"+    resolveTop+    modifyCardDeprecated "Growth-Chamber Guardian 2" cardPlusOneCounters (+ 1)+    trigger "Search" "Growth-Chamber Guardian 1"+    resolveTop++    resolveTop+    gainAttribute haste "Growth-Chamber Guardian 2"++  step "Cast Animus on Egg. Enables Piston-Fist, triggers Harpy" $ do+    tapForMana "G" "Overgrown Tomb 3"+    tapForMana "R" "Blood Crypt 3"+    cast "1G" "Ancient Animus" >> resolveTop+    target "Dragon Egg"++    loseAttribute defender "Piston-Fist Cyclops"++    fight "Dragon Egg" "Goring Ceratops"+    trigger "Make Dragon" "Dragon Egg"+    trigger "+1/+1" "Mausoleum Harpy"++    resolveHarpyTrigger++    resolveTop+    withLocation Play $ do+      withAttributes [token, flying, summoned] $ addCreature (2, 2) "Dragon"++  step "Attack with all except dragon" $ do+    attackWith+      [ "Mausoleum Harpy"+      , "Piston-Fist Cyclops"+      , "Growth-Chamber Guardian 1"+      , "Growth-Chamber Guardian 2"+      ]++  fork "Opponent doesn't block one ground attacker, damage is lethal" $ do+    step "First-strike damage" $ do+      combatDamage ["Goring Ceratops"] "Piston-Fist Cyclops"+      combatDamage ["Goblin Chainwhirler 1"] "Growth-Chamber Guardian 1"++    step "Death triggers for harpy" $ do+      trigger "Cyclops" "Mausoleum Harpy"+      trigger "Guardian 1" "Mausoleum Harpy"++      resolveTop >> modifyStrength (1, 1) "Mausoleum Harpy"+      resolveTop >> modifyStrength (1, 1) "Mausoleum Harpy"++    step "Regular damage" $ do+      combatDamage [] "Mausoleum Harpy"+      combatDamage [] "Growth-Chamber Guardian 2"+      validateLife (-2) Opponent++  step "First-strike damage" $ do+    combatDamage ["Goring Ceratops"] "Piston-Fist Cyclops"+    combatDamage ["Goblin Chainwhirler 1"] "Growth-Chamber Guardian 1"+    combatDamage ["Goblin Chainwhirler 2"] "Growth-Chamber Guardian 2"++  step "Death triggers for harpy" $ do+    trigger "Cyclops" "Mausoleum Harpy"+    trigger "Guardian 1" "Mausoleum Harpy"+    trigger "Guardian 2" "Mausoleum Harpy"++    resolveTop >> modifyStrength (1, 1) "Mausoleum Harpy"+    resolveTop >> modifyStrength (1, 1) "Mausoleum Harpy"+    resolveTop >> modifyStrength (1, 1) "Mausoleum Harpy"++  step "Regular damage" $ do+    combatDamage [] "Mausoleum Harpy"+    validateLife 0 Opponent++formatter step = attributes <> case view stepNumber step of+  1 -> boardFormatter+  5 -> cardFormatter+        "attacking creatures"+        (matchLocation (Active, Play)+        <> matchAttribute attacking+        )+  _ -> mempty++resolveHarpyTrigger = do+  resolveTop+  modifyCardDeprecated "Mausoleum Harpy" cardPlusOneCounters (+ 1)++attributes = attributeFormatter $ do+  attribute "life" $ countLife Opponent+  attribute "mana" $ countCards $ matchAttribute land <> missingAttribute tapped+  attribute "harpy" $+    view cardStrength <$> requireCard "Mausoleum Harpy" mempty++blocked = "blocked"
src/Solutions/RivalsOfIxalan7.hs view
@@ -23,7 +23,7 @@             (    matchLocation . view cardLocation               <> const (matchAttribute creature)             )-            (pure . over cardStrength (mkStrength (1, 1) <>))+            (pure . over cardStrengthModifier (mkStrength (1, 1) <>))         $ addCreature (2, 2) "Tah-Crop Elite"      withLocation (Active, Hand) $ do@@ -64,7 +64,7 @@     trigger "Tilonalli's Skinshifter"     target "Tah-Crop Elite"     validate "Tah-Crop Elite" $ matchAttribute attacking-    resetStrength "Tilonalli's Skinshifter" (2, 2)+    copyAttributes "Tah-Crop Elite" "Tilonalli's Skinshifter"     gainAttribute flying "Tilonalli's Skinshifter"    fork@@ -132,3 +132,15 @@     <> (missingAttribute blocked `matchOr` matchAttribute trample)     ) formatter _ = attributes++copyAttributes :: CardName -> CardName -> GameMonad ()+copyAttributes from to = do+  toCard <- requireCard to (matchAttribute creature)++  -- Can't use requireCard because need access to the base strength+  maybeCard <- use $ cards . at from++  case maybeCard of+    Nothing -> throwError $ "Card does not exist: " <> from+    Just (BaseCard card) -> do+      modifyCard cardStrength (const $ view cardStrength card) to
src/Solutions/UltimateMasters.hs view
@@ -1,6 +1,6 @@ module Solutions.UltimateMasters where -import Control.Lens (over)+import Control.Lens (over, view) import Control.Monad  import Dovin.V2@@ -64,7 +64,7 @@      cast "B" "Reanimate" >> resolveTop     targetInLocation (Active, Graveyard) "Vengevine"-    returnToPlay "Vengevine"+    moveTo Play "Vengevine"    step "Sac Vengevine (undying) then Mikaeus for mana" $ do     sacrificeToAltar "U" "Vengevine"@@ -89,7 +89,7 @@     with "Mikaeus, the Unhallowed" $ \cn -> do       targetInLocation (Active, Graveyard) cn       validate (matchAttribute legendary) cn-      returnToPlay cn+      moveTo Play cn       gainAttribute haste cn      with "Stingerfling Spider" $ \cn -> do@@ -103,7 +103,7 @@       destroy "Sublime Archangel"    step "Liliana to force sac of Reya" $ do-    activatePlaneswalker 1 "Liliana of the Veil"+    activatePlaneswalker "" 1 "Liliana of the Veil" >> resolveTop     as Opponent $ do       sacrifice "Reya Dawnbringer" @@ -169,7 +169,7 @@     cast "4GR" "Vengeful Rebirth" >> resolveTop      targetInLocation (Active, Graveyard) "Mikaeus, the Unhallowed"-    returnToHand "Mikaeus, the Unhallowed"+    moveTo Hand "Mikaeus, the Unhallowed"     damage (const 6) (targetPlayer Opponent) "Vengeful Rebirth"      validateLife 0 Opponent@@ -181,44 +181,47 @@             <> matchController Active             )       <*> countManaPool Active-formatter 1 = boardFormatter-formatter 3 = manaAttribute++formatter = oldFormatter . view stepNumber++oldFormatter 1 = boardFormatter+oldFormatter 3 = manaAttribute   <> cardFormatter        "creatures"        (matchLocation (Active, Play) <> matchAttribute creature)   <> cardFormatter        "graveyard"        (matchLocation (Active, Graveyard) <> matchAttribute creature)-formatter 4 = manaAttribute+oldFormatter 4 = manaAttribute   <> cardFormatter        "remaining creatures"        (matchLocation (Opponent, Play) <> matchAttribute creature)-formatter 5 = manaAttribute+oldFormatter 5 = manaAttribute   <> cardFormatter        "remaining creatures"        (matchLocation (Opponent, Play) <> matchAttribute creature)-formatter 6 = manaAttribute+oldFormatter 6 = manaAttribute   <> cardFormatter        "remaining creatures"        (matchLocation (Opponent, Play) <> matchAttribute creature)-formatter 8 =+oldFormatter 8 =   cardFormatter     "creatures"     (matchLocation (Active, Play) <> matchAttribute creature)-formatter 9 =+oldFormatter 9 =   cardFormatter     "unblocked creatures"     (matchLocation (Active, Play)     <> matchAttribute creature     <> invert (matchAttribute "blocked")     )-formatter 10 =+oldFormatter 10 =      attributeFormatter (attribute "life" (countLife Opponent))   <> cardFormatter        "creatures"        (matchLocation (Active, Play) <> matchAttribute creature)-formatter 11 = manaAttribute+oldFormatter 11 = manaAttribute   <> attributeFormatter (attribute "life" (countLife Opponent))-formatter 12 = manaAttribute+oldFormatter 12 = manaAttribute   <> attributeFormatter (attribute "life" (countLife Opponent))-formatter _ = manaAttribute+oldFormatter _ = manaAttribute
+ src/Solutions/WarOfTheSpark2.hs view
@@ -0,0 +1,155 @@+module Solutions.WarOfTheSpark2 where++import Dovin.V2+import Dovin.Prelude++dinosaur = "dinosaur"+tricked = "tricked"++solution :: GameMonad ()+solution = do+  step "Initial state" $ do+    setLife Opponent 4++    withLocation Hand $ do+      addAura "Kaya's Ghostform"+      withAttribute haste $ addCreature (1, 1) "Dreadhorde Butcher"+      addCreature (2, 2) "Merfolk Trickster"+      addCreature (4, 4) "Massacre Girl"++    withLocation Play $ do+      addLands 4 "Steam Vents"+      addLands 4 "Blood Crypt"++      withPlusOneCounters 2 $ addCreature (2, 2) "Flux Channeler"+      withPlusOneCounters 3 $ withAttribute firststrike $ addCreature (1, 1) "Rigging Runner"+      withPlusOneCounters 1 $ addCreature (2, 2) "Spellgorger Weird"++    as Opponent $ do+      withLocation Play $ do+        addCreature (3, 2) "Midnight Reaper"+        addCreature (1, 1) "Drover of the Mighty"++        -- HACK: Model Drover's passive ability as an ability of Zetalpa. Don't+        -- currently have a way of checking _other_ cards to figure out if+        -- effect applies.+        withEffect+          matchInPlay+          (pure $ matchName "Drover of the Mighty" <> missingAttribute tricked)+          (pure . over cardStrengthModifier (mkStrength (2, 2) <>)) $+            withAttributes [indestructible] $+              addCreature (4, 8) "Zetalpa, Primal Dawn"+        addCreature (5, 5) "God-Eternal Rhonas"++  step "Cast Merfolk Trickster, targeting Drover" $ do+    tapForMana "U" "Steam Vents 1"+    tapForMana "U" "Steam Vents 2"++    cast "UU" "Merfolk Trickster" >> resolveTop++    target "Drover of the Mighty"+    gainAttribute tricked "Drover of the Mighty"+    tap "Drover of the Mighty"++  step "Cast Kaya's Ghostform on Merfolk Trickster" $ do+    tapForMana "B" "Blood Crypt 1"++    cast "B" "Kaya's Ghostform"+    target "Merfolk Trickster"++    trigger "Proliferate" "Flux Channeler"+    trigger "+1/+1" "Spellgorger Weird"++    resolve "+1/+1"+    modifyCard cardPlusOneCounters (+ 1) "Spellgorger Weird"++    resolve "Proliferate"+    modifyCard cardPlusOneCounters (+ 1) "Spellgorger Weird"+    modifyCard cardPlusOneCounters (+ 1) "Rigging Runner"+    modifyCard cardPlusOneCounters (+ 1) "Flux Channeler"++    resolve "Kaya's Ghostform"++  step "Cast Massacre Girl, killing Drover" $ do+    tapForMana "B" "Blood Crypt 2"+    tapForMana "B" "Blood Crypt 3"+    tapForMana "B" "Blood Crypt 4"+    tapForMana "U" "Steam Vents 3"+    tapForMana "U" "Steam Vents 4"++    cast "3BB" "Massacre Girl" >> resolveTop++    withStateBasedActions $ do+      forCards (matchInPlay <> matchAttribute creature <> invert (matchName "Massacre Girl")) $ \cn -> do+        modifyCard cardStrengthModifier (mkStrength (-1, -1) <>) cn++    validate (matchLocation (Opponent, Graveyard)) "Drover of the Mighty"++    trigger "-1/-1" "Massacre Girl"+    as Opponent $ do+      trigger "Self-damage" "Midnight Reaper"++  step "Resolve midnight reaper damage" $ do+    resolve "Self-damage"++    loseLife Opponent 1++  step "Resolve -1/-1, killing Midnight Reaper and Trickster" $ do+    resolve "-1/-1"++    withStateBasedActions $ do+      forCards (matchInPlay <> matchAttribute creature <> invert (matchName "Massacre Girl")) $ \cn -> do+        modifyCard cardStrengthModifier (mkStrength (-1, -1) <>) cn++    -- Active player triggers stack first+    validate (matchLocation (Active, Graveyard)) "Merfolk Trickster"+    validate (matchLocation (Opponent, Graveyard)) "Midnight Reaper"+    moveTo Graveyard "Kaya's Ghostform"++    trigger "Return Trickster" "Kaya's Ghostform"+    trigger "-1/-1 Trickster" "Massacre Girl"+    trigger "-1/-1 Reaper" "Massacre Girl"+    as Opponent $ do+      trigger "Self-damage" "Midnight Reaper"++  step "Resolve midnight reaper damage" $ do+    resolve "Self-damage"++    loseLife Opponent 1++  step "Resolve -1/-1 for Reaper" $ do+    resolve "-1/-1 Reaper"++    withStateBasedActions $ do+      forCards (matchInPlay <> matchAttribute creature <> invert (matchName "Massacre Girl")) $ \cn -> do+        modifyCard cardStrengthModifier (mkStrength (-1, -1) <>) cn++  step "Resolve -1/-1 for Trickster" $ do+    resolve "-1/-1 Trickster"++    withStateBasedActions $ do+      forCards (matchInPlay <> matchAttribute creature <> invert (matchName "Massacre Girl")) $ \cn -> do+        modifyCard cardStrengthModifier (mkStrength (-1, -1) <>) cn++  step "Return Trickster to play from Ghostform, targeting God-Eternal" $ do+    moveTo Play "Merfolk Trickster"++    target "God-Eternal Rhonas"+    tap "God-Eternal Rhonas"+    gainAttribute tricked "God-Eternal Rhonas"++  step "Attack with all original creatures, opponent can only block 1 and can't kill it" $ do+    attackWith ["Flux Channeler", "Rigging Runner", "Spellgorger Weird"]++    -- First strike damage+    combatDamage ["Zetalpa, Primal Dawn"] "Rigging Runner"++    -- Regular damage+    combatDamage [] "Flux Channeler"+    combatDamage [] "Spellgorger Weird"++    validateLife 0 Opponent++attributes = attributeFormatter $ do+  attribute "life" $ countLife Opponent+formatter _ = attributes <> boardFormatter
+ test/ActivatePlaneswalker.hs view
@@ -0,0 +1,32 @@+module ActivatePlaneswalker where++import TestPrelude.V2++test_ActivatePlaneswalker = testGroup "activatePlaneswalker"+  [ prove "adds activated card to stack" $ do+      withLocation Play $ addPlaneswalker 4 "Karn, Scion of Urza"+      activatePlaneswalker "Reveal cards" (-1) "Karn, Scion of Urza"++      validate+        (matchLocation (Active, Stack) <> matchAttribute activated)+        "Reveal cards"+      validate (matchLoyalty 3) "Karn, Scion of Urza"++      resolve "Reveal cards"+  , refute+      "requires card in play"+      "in play" $ do+        withLocation Graveyard $ addPlaneswalker 4 "Karn, Scion of Urza"+        activatePlaneswalker "Reveal cards" (-1) "Karn, Scion of Urza"+  , refute+      "requires card controlled by actor"+      "has controller Opponent" $ do+        withLocation Play $ addPlaneswalker 4 "Karn, Scion of Urza"+        as Opponent $+          activatePlaneswalker "Reveal cards" (-1) "Karn, Scion of Urza"+  , refute+      "requires sufficient loyalty"+      "does not have enough loyalty" $ do+        withLocation Play $ addPlaneswalker 1 "Karn, Scion of Urza"+        activatePlaneswalker "Create construct" (-2) "Karn, Scion of Urza"+  ]
+ test/CopySpell.hs view
@@ -0,0 +1,20 @@+module CopySpell where++import TestPrelude++test_CopySpell = testGroup "copySpell"+  [ prove "copies a spell on the stack" $ do+      withLocation (Active, Hand) $ addInstant "Shock"+      cast "" "Shock"++      copySpell "Shock (copy)" "Shock"++      resolve "Shock (copy)"+      resolve "Shock"+  , refute+      "spell must be on stack"+      "on stack" $ do+        withLocation (Active, Hand) $ addInstant "Shock"++        copySpell "Shock (copy)" "Shock"+  ]
+ test/Destroy.hs view
@@ -0,0 +1,20 @@+module Destroy where++import TestPrelude.V2++test_Destroy = testGroup "destroy"+  [ prove "destroys a card in play" $ do+      withLocation Play $ addArtifact "Mox"+      destroy "Mox"+      validate (matchLocation (Active, Graveyard)) "Mox"+  , refute+      "cannot destroy indestructible"+      "not has attribute indestructible" $ do+        withLocation Play $ withAttribute indestructible $ addArtifact "Mox"+        destroy "Mox"+  , refute+      "requires in play"+      "in play" $ do+        withLocation Graveyard $ addArtifact "Mox"+        destroy "Mox"+  ]
test/Driver.hs view
@@ -1,1 +1,1 @@-{-# OPTIONS_GHC -F -pgmF tasty-discover #-}+{-# OPTIONS_GHC -F -pgmF tasty-discover -optF --hide-successes  #-}
+ test/Fight.hs view
@@ -0,0 +1,46 @@+module Fight where++import TestPrelude++test_Fight = testGroup "fight"+  [ prove "adds damage to both creatures" $ do+      withLocation (Active, Play) $ addCreature (2, 4) "Angel 1"+      withLocation (Active, Play) $ addCreature (3, 4) "Angel 2"++      fight "Angel 1" "Angel 2"+      validate "Angel 1" $ matchDamage 3+      validate "Angel 2" $ matchDamage 2+  , prove "fighting self adds double damage" $ do+      withLocation (Active, Play) $ addCreature (2, 4) "Angel 1"++      fight "Angel 1" "Angel 1"+      validate "Angel 1" $ matchDamage 4+  , refute+      "require first card to be in play"+      "in play" $ do+        withLocation (Active, Graveyard) $ addCreature (2, 4) "Angel 1"+        withLocation (Active, Play) $ addCreature (3, 4) "Angel 2"++        fight "Angel 1" "Angel 2"+  , refute+      "require second card to be in play"+      "in play" $ do+        withLocation (Active, Play) $ addCreature (2, 4) "Angel 1"+        withLocation (Active, Graveyard) $ addCreature (3, 4) "Angel 2"++        fight "Angel 1" "Angel 2"+  , refute+      "require first card to be a creature"+      "has attribute creature" $ do+        withLocation (Active, Graveyard) $ addEnchantment "Angel 1"+        withLocation (Active, Play) $ addCreature (3, 4) "Angel 2"++        fight "Angel 1" "Angel 2"+  , refute+      "require second card to be a creature"+      "has attribute creature" $ do+        withLocation (Active, Play) $ addCreature (2, 4) "Angel 1"+        withLocation (Active, Graveyard) $ addEnchantment "Angel 2"++        fight "Angel 1" "Angel 2"+  ]
+ test/ModifyStrength.hs view
@@ -0,0 +1,39 @@+module ModifyStrength where++import TestPrelude.V2++test_ModifyStrength = testGroup "modifyStrength"+  [ prove "modifies effective strength of creature" $ do+      withLocation Play $ addCreature (1, 1) "Soldier"+      modifyStrength (1, 2) "Soldier"++      c <- requireCard "Soldier" mempty++      unless (view cardStrength c == mkStrength (2, 3)) $+        throwError "Did not modify strength"+  , prove "strength modification reset when card leaves play" $ do+      withLocation Play $ addCreature (1, 1) "Soldier"+      modifyStrength (1, 2) "Soldier"++      withStateBasedActions $ moveTo Graveyard "Soldier"++      c <- requireCard "Soldier" mempty++      unless (view cardStrength c == mkStrength (1, 1)) $+        throwError "Did not reset strength"+  , prove "works with SBAs to graveyard creature for <= 0 toughness" $ do+      withLocation Play $ addCreature (1, 1) "Soldier"+      withStateBasedActions $ modifyStrength (0, -1) "Soldier"++      validate (matchLocation (Active, Graveyard)) "Soldier"+  , refute+      "requires in play"+      "in play" $ do+        withLocation Hand $ addCreature (1, 1) "Soldier"+        modifyStrength (0, -1) "Soldier"+  , refute+      "requires creature"+      "has attribute creature" $ do+        withLocation Hand $ addArtifact "Soldier"+        modifyStrength (0, -1) "Soldier"+  ]
test/Move.hs view
@@ -51,7 +51,7 @@         matchLocation (Active, Graveyard) <> matchPlusOneCounters 0   , prove "removes damage when leaving play" $ do       withLocation (Active, Play) $ addCreature (1, 1) "Zombie"-      modifyCard "Zombie" cardDamage (const 1)+      modifyCardDeprecated "Zombie" cardDamage (const 1)        moveTo Graveyard "Zombie" @@ -97,7 +97,7 @@         withLocation (Active, Hand) $ addInstant "Shock"          cast "" "Shock"-        copySpell "Shock" "Shock 1" >> resolveTop+        copySpell "Shock 1" "Shock" >> resolveTop          move (Active, Graveyard) (Active, Play) "Shock 1"   ]
+ test/Sacrifice.hs view
@@ -0,0 +1,20 @@+module Sacrifice where++import TestPrelude.V2++test_Sacrifice = testGroup "sacrifice"+  [ prove "moves card to graveyard" $ do+      withLocation Play $ addArtifact "Mox"+      sacrifice "Mox"+      validate (matchLocation (Active, Graveyard)) "Mox"+  , refute+      "cannot sacrifice permanents owned by opponent"+      "has controller Active" $ do+        as Opponent $ withLocation Play $ addArtifact "Mox"+        sacrifice "Mox"+  , refute+      "requires in play"+      "in play" $ do+        withLocation Graveyard $ addArtifact "Mox"+        destroy "Mox"+  ]
test/Spec.hs view
@@ -116,32 +116,6 @@         addMana "10"         validateBoardEquals (manaPool . at Active . _Just) (replicate 10 'X')     ]-  , testGroup "tap"-    [ prove "taps card in play" $ do-        withLocation (Active, Play) $ addLand "Forest"-        tap "Forest"--        validate "Forest" $ matchAttribute tapped-    , prove "taps card in opponent's play" $ do-        withLocation (Active, Play) $ addLand "Forest"-        tap "Forest"--        validate "Forest" $ matchAttribute tapped-    , refute-        "requires card exists"-        "Card does not exist: Forest" $ do-          tap "Forest"-    , refute-        "requires untapped"-        "not has attribute tapped" $ do-          withAttribute tapped $ withLocation (Active, Play) $ addLand "Forest"-          tap "Forest"-    , refute-        "requires in play"-        "in play" $ do-          withLocation (Active, Graveyard) $ addLand "Forest"-          tap "Forest"-    ]   , testGroup "tapForMana"     [ prove "taps card and adds mana to pool" $ do         withLocation (Active, Play) $ addLand "Forest"@@ -280,6 +254,13 @@             withAttributes [summoned] $ addCreature (1, 1) "Bat"            attackWith ["Bat"]+    , refute+        "prevents defenders from attacking"+        "not has attribute defender" $ do+          withLocation (Active, Play) $ do+            withAttributes [defender] $ addCreature (1, 1) "Wall"++          attackWith ["Wall"]     , prove "allows hasty creatures to attack" $ do         withLocation (Active, Play) $ do           withAttributes [haste, summoned] $ addCreature (1, 1) "Bat"
test/StateBasedActions.hs view
@@ -1,60 +1,68 @@ module StateBasedActions where -import TestPrelude+import TestPrelude.V2  test_SBAs = testGroup "state-based actions"   [ prove "destroys creature with damage exceeding toughness" $ do       withStateBasedActions $ do-        withLocation (Active, Play)+        withLocation Play           $ addCreature (1, 1) "Spirit"          damage (const 2) (targetCard "Spirit") "Spirit" -      validate "Spirit" $ matchLocation (Active, Graveyard)+      validate (matchLocation (Active, Graveyard)) "Spirit"   , prove "destroys deathtouched creature" $ do       withStateBasedActions $ do-        withLocation (Active, Play)+        withLocation Play           $ withAttribute deathtouched           $ addCreature (1, 1) "Spirit" -      validate "Spirit" $ matchLocation (Active, Graveyard)+      validate (matchLocation (Active, Graveyard)) "Spirit"   , prove "does not destroy indestructible creature" $ do       withStateBasedActions $ do-        withLocation (Active, Play)+        withLocation Play           $ withAttributes [indestructible, deathtouched]           $ addCreature (1, 1) "Spirit"          damage (const 2) (targetCard "Spirit") "Spirit" -      validate "Spirit" $ matchInPlay+      validate matchInPlay "Spirit"    , prove "removes tokens not in play" $ do        withStateBasedActions $ do-         withLocation (Active, Graveyard)+         withLocation Graveyard            $ withAttribute token            $ addArtifact "Treasure"         validateRemoved "Treasure"    , prove "does not remove copies on stack" $ do        withStateBasedActions $ do-         withLocation (Active, Hand)+         withLocation Hand            $ withAttribute copy            $ addArtifact "Shock"          cast "" "Shock" -       validate "Shock" $ matchLocation (Active, Stack)+       validate (matchLocation (Active, Stack)) "Shock"    , prove "removes copies not on stack" $ do        withStateBasedActions $ do-         withLocation (Active, Graveyard)+         withLocation Graveyard            $ withAttribute copy            $ addArtifact "Shock"         validateRemoved "Shock"    , prove "correctly removes damaged tokens" $ do        withStateBasedActions $ do-         withLocation (Active, Play)+         withLocation Play            $ withAttribute token            $ addCreature (1, 1) "Spirit"           damage (const 1) (targetCard "Spirit") "Spirit"        validateRemoved "Spirit"+   , prove "cancels out +1/+1 and -1/-1 counters" $ do+       withStateBasedActions $ do+         withLocation Play+           $ withPlusOneCounters 3+           $ withMinusOneCounters 2+           $ addCreature (5, 5) "Dinosaur"++       validate (matchPlusOneCounters 1 <> matchMinusOneCounters 0) "Dinosaur"   ]
+ test/Tap.hs view
@@ -0,0 +1,43 @@+module Tap where++import TestPrelude.V2++test_Tap = testGroup "tap"+  [ prove "taps card in play" $ do+      withLocation Play $ addLand "Forest"+      tap "Forest"++      validate (matchAttribute tapped) "Forest"+  , prove "taps card in opponent's play" $ do+      withLocation Play $ addLand "Forest"+      tap "Forest"++      validate (matchAttribute tapped) "Forest"+  , prove "taps creature without summoning sickness" $ do+     withLocation Play $ addCreature (1, 1) "Goblin"+     tap "Goblin"+  , prove "taps creature with haste" $ do+     withLocation Hand $ withAttribute haste $ addCreature (1, 1) "Goblin"+     cast "" "Goblin" >> resolveTop+     tap "Goblin"+  , refute+      "requires card exists"+      "Card does not exist: Forest" $ do+        tap "Forest"+  , refute+      "requires untapped"+      "not has attribute tapped" $ do+        withAttribute tapped $ withLocation Play $ addLand "Forest"+        tap "Forest"+  , refute+      "requires in play"+      "in play" $ do+        withLocation Graveyard $ addLand "Forest"+        tap "Forest"+  , refute+     "requires creature not summoned"+     "does not have summoning sickness" $ do+       withLocation Hand $ addCreature (1, 1) "Soldier"+       cast "" "Soldier" >> resolveTop+       tap "Soldier"+  ]
+ test/Target.hs view
@@ -0,0 +1,23 @@+module Target where++import TestPrelude.V2++test_Target = testGroup "target"+  [ prove "targets a card in play" $ do+      withLocation Play $ addArtifact "Mox"+      target "Mox"+  , prove "targets own card with hexproof" $ do+      withLocation Play $ withAttribute hexproof $ addArtifact "Mox"+      target "Mox"+  , refute+      "cannot target hexproof of opponent"+      "not has attribute hexproof" $ do+        as Opponent $ do+          withLocation Play $ withAttribute hexproof $ addArtifact "Mox"+        target "Mox"+  , refute+      "requires in play"+      "in play" $ do+        withLocation Graveyard $ addArtifact "Mox"+        target "Mox"+  ]
+ test/TargetInLocation.hs view
@@ -0,0 +1,14 @@+module TargetInLocation where++import TestPrelude.V2++test_Target = testGroup "targetInLocation"+  [ prove "targets a card in graveyard" $ do+      withLocation Graveyard $ addArtifact "Mox"+      targetInLocation (Active, Graveyard) "Mox"+  , refute+      "requires location"+      "in location (Active,Graveyard)" $ do+        withLocation Play $ addArtifact "Mox"+        targetInLocation (Active, Graveyard) "Mox"+  ]
test/TestPrelude.hs view
@@ -9,6 +9,7 @@   , prove   , refute   , validateBoardEquals+  , throwError   ) where  import Test.Tasty@@ -23,7 +24,6 @@ import Control.Lens import Control.Monad.Except - prove name m = testCase name $   case runMonad emptyBoard m of     (Left msg, _, _) -> assertFailure msg@@ -39,4 +39,3 @@    unless (x == expected) $     throwError ("want: " <> show expected <> ", got: " <> show x)-
test/TestPrelude/V2.hs view
@@ -10,4 +10,5 @@   , validate   , validateLife   , withLocation+  , fork   )