diff --git a/battlegrounds.cabal b/battlegrounds.cabal index d1b6615..9addfe8 100644 --- a/battlegrounds.cabal +++ b/battlegrounds.cabal @@ -44,6 +44,7 @@ library MonadRandom , base >=4.7 && <5 , containers ==0.6.7 + , free ==5.2 , large-generics ==0.2.2 , large-records ==0.4.1 , mtl ==2.3.1 diff --git a/docs/Thoughts.md b/docs/Thoughts.md index e42b37a..00d38dc 100644 --- a/docs/Thoughts.md +++ b/docs/Thoughts.md @@ -56,7 +56,7 @@ Refactoring: The API calls for many redesign opprotunities: Today's result: Combat.hs typechecks. Claude suggests to refactor `CombatState` into a map of `{ One: one's fighterstate, Two: two's fighterstate }` -### Oct 13, 2024: +### Oct 13, 2024: Rando Thoughts Been reading Granin's FDD book. Main question I'm having: 1. What types can be "correct by construction"? @@ -73,3 +73,25 @@ Questions from 8/5/2024 still persist: Thought: Another question: + +### Oct 14, 2024: + +What if language is actually a continuation-based eDSL? + +``` +PickTarget (\target -> cont) +Consume Target -- Somehow utilize "GainStats" here? + + +``` + +### Oct 19, 2024: +I actually free-monadized a lot of the language! + +Design Challenge: +- How to model "UpTo" and "AfterPlay" logic in one statement? +Snail Cavalry requires this, but with both `UpTo` and `AfterPlay` being +`Functionalities`, they themselves do not compose. + +Resolution: +- Split `Functionalities` into `KeywordFunctionality`, `EventFunctionality`, and `FunctionalityCombinator`. diff --git a/package.yaml b/package.yaml index f276d4f..ddf23dc 100644 --- a/package.yaml +++ b/package.yaml @@ -48,6 +48,7 @@ library: - large-generics == 0.2.2 - large-records == 0.4.1 - record-hasfield + - free == 5.2 executables: diff --git a/src/DesignSketch.hs b/src/DesignSketch.hs index b39ebae..b0bddc2 100644 --- a/src/DesignSketch.hs +++ b/src/DesignSketch.hs @@ -1,91 +1,104 @@ -- Temp file. Place for some all-in-one mocks +{-# LANGUAGE DeriveFunctor #-} -module TempDesignSketch (module TempDesignSketch) where +module DesignSketch (module DesignSketch) where --- Sources of randomness: --- 1. Random defender selection --- 2. Random shop rolls --- 3. Choose random minion in tavern (picky eater, enchanted lasso) --- 4. Choose random minion in opponent's warband (cry foul) --- 4. Choose random card under some additional criterion (chef's choice, shell whistler) +import Control.Monad.Free -type Multiplier = Int +data CardFilterCriterion = MaxTier Int | Tribe Tribe | IsMinion -data RandomTarget - = HandRandom - | HandLeftmost - | HandRightmost - | BoardRandom - | ShopRandom - deriving (Eq) +data RandomTarget = Hand | Shop | Board -data RandomCriterion = RTribe Tribe | RTarget RandomTarget | RTier Int deriving (Eq) +data InjectAvatarMethod next + = QueryTier (Int -> next) + | MakeRandomCard [CardFilterCriterion] (CardInstance -> next) + | TargetRandomCard RandomTarget [CardFilterCriterion] (Either EffectError CardInstance -> next) + | TargetRandomCards RandomTarget [CardFilterCriterion] Int (Either EffectError [CardInstance] -> next) + deriving (Functor) -data CounterCriterion - = -- Upbeat Frontdrake - EndOfTurn - | -- Avenge mechanic - FriendlyDeaths - deriving (Eq) +type InjectAvatar a = Free InjectAvatarMethod a --- Unused yet. -data Target - = TShop Int - | THand Int - | -- Them Apples. - TEntireShop +queryTier :: InjectAvatar Int +queryTier = liftF $ QueryTier id -data TargetedEffect -- TODO: The idea of a target might be important. +makeRandomCard :: [CardFilterCriterion] -> InjectAvatar CardInstance +makeRandomCard criterions = liftF $ MakeRandomCard criterions id --- TODO: Something is missing. --- +targetRandomCard :: RandomTarget -> [CardFilterCriterion] -> InjectAvatar (Either EffectError CardInstance) +targetRandomCard randTarget crits = liftF $ TargetRandomCard randTarget crits id -data InjectAvatar cont - = QueryTier (Int -> cont) - | QueryHealth (Int -> cont) +targetRandomCards :: RandomTarget -> [CardFilterCriterion] -> Int -> InjectAvatar (Either EffectError [CardInstance]) +targetRandomCards randomTarget crits count = liftF $ TargetRandomCards randomTarget crits count id data StateEffect - = -- E.g., Glim Guardian - GainStats Int Int + = -- E.g., Trusty Pup + GainPermStats Stats + | -- E.g., Stats gained during Combat (Glim Guardian); Spellcraft + GainTempStats Stats + | -- E.g., Ancestral Automaton, Eternal Knight, (Maybe) Deep Blue + GainBaseStats Stats + | GainTaunt | -- E.g., Cord Puller Summon CardName | -- E.g., Backstage Security DamageHero Int | -- E.g., Upbeat Frontdrake - GetRandom [RandomCriterion] - | -- E.g., Geomancer - Get CardName - | -- E.g., Picky Eater - Consume Multiplier + AddToHand CardInstance + | -- E.g., Lasso + RemoveFromShop CardInstance + | Take CardInstance | -- E.g., Tavern Coin GainGold Int + +data Tribe = Murloc | Dragon | Demon | Elemental | Undead | Mech | Naga | SpellTODO deriving (Eq) + +data CounterType + = -- Upbeat Frontdrake + EndOfTurn + | -- Avenge mechanic + FriendlyDeaths + | -- Tehhys + GoldSpent + | -- Elise + Refresh deriving (Eq) -data Tribe = Murloc | Dragon | Demon deriving (Eq) +type EffectError = String --- TODO: It has became obvious what are "keywords" and what are "functionalities". Just observe who has StateEffect. -data Functionality - = -- phase=Combat - Taunt +type Count = Int + +data Per = PerCombat | PerRecruit | PerGame + +data KeywordFunctionality + = Taunt | DivineShield - | Deathrattle [StateEffect] - | OnAttack [StateEffect] - | OnDamaged [StateEffect] - | OnKill [StateEffect] - | OnSummon [StateEffect] - | OnSell [StateEffect] - | StartOfCombat [StateEffect] | Reborn | Windfury - | -- phase=Recruit - Battlecry StateEffect - | AfterPlay [StateEffect] + | Deathrattle (InjectAvatar (Either EffectError [StateEffect])) + | StartOfCombat (InjectAvatar (Either EffectError [StateEffect])) + | Battlecry (InjectAvatar (Either EffectError [StateEffect])) | Spellcraft Card - | -- Both - AfterSummon Tribe StateEffect - | -- Combinator - Counter CounterCriterion Int StateEffect - deriving (Eq) + +data EventFunctionality + = -- Events detectable by "inspecting" the card itself + OnAttack (InjectAvatar (Either EffectError [StateEffect])) + | OnDamaged (InjectAvatar (Either EffectError [StateEffect])) + | OnKill (InjectAvatar (Either EffectError [StateEffect])) + | OnSell (InjectAvatar (Either EffectError [StateEffect])) + | -- Events that are detectable only by listening onto some other more "global" events + AfterPlay (Card -> Bool) (InjectAvatar (Either EffectError [StateEffect])) + | AfterSummon (Card -> Bool) (InjectAvatar (Either EffectError [StateEffect])) + | -- Every `count` times `counterType` happens, run effects + Every Count CounterType (InjectAvatar (Either EffectError [StateEffect])) + +data FunctionalityCombinator + = -- Per `Per`, run effect up to `Count` times. + UpTo Count Per EventFunctionality + +data Functionality + = Keyword KeywordFunctionality + | Event EventFunctionality + | Combinator FunctionalityCombinator data Stats = Stats Int Int deriving (Eq) @@ -98,46 +111,135 @@ data CardName | UpbeatFrontdrake | EnchantedLasso | MisfitDragonling + | MoltenRock + | PickyEater + | DeepseaAngler + | AnglersLure + | SnailCavalry deriving (Eq) -data Card = Card CardName Stats [Functionality] deriving (Eq) +data Card = Card + { cardName :: CardName, + stats :: Stats, + tribe :: Tribe, + functionality :: [Functionality] + } -newtype CardInstance = CardInstance Card +newtype CardInstance = CardInstance {card :: Card} glimGuardian :: Card -glimGuardian = Card GlimGuardian (Stats 1 4) [OnAttack [GainStats 2 1]] +glimGuardian = Card GlimGuardian (Stats 1 4) Dragon [Event $ OnAttack (return $ Right [GainTempStats (Stats 2 1)])] skeleton :: Card -skeleton = Card Skeleton (Stats 1 1) [] +skeleton = Card Skeleton (Stats 1 1) Undead [] harmlessBonehead :: Card -harmlessBonehead = Card HarmlessBonehead (Stats 1 1) [Deathrattle [Summon Skeleton, Summon Skeleton]] +harmlessBonehead = Card HarmlessBonehead (Stats 1 1) Undead [Keyword $ Deathrattle (return $ Right [Summon Skeleton, Summon Skeleton])] microbot :: Card -microbot = Card Microbot (Stats 1 1) [] +microbot = Card Microbot (Stats 1 1) Mech [] cordPuller :: Card -cordPuller = Card CordPuller (Stats 1 1) [DivineShield, Deathrattle [Summon Microbot]] +cordPuller = Card CordPuller (Stats 1 1) Mech [Keyword DivineShield, Keyword $ Deathrattle (return $ Right [Summon Microbot])] upbeatFrontdrake :: Card -upbeatFrontdrake = Card UpbeatFrontdrake (Stats 1 1) [Counter EndOfTurn 3 (GetRandom [RTribe Dragon])] - --- Look mom! Tavern spells can be modeled as cards. +upbeatFrontdrake = + Card + UpbeatFrontdrake + (Stats 1 1) + Dragon + [ Event $ + Every + 3 + EndOfTurn + ( do + t <- queryTier + card <- makeRandomCard [MaxTier t, Tribe Dragon] + return $ Right [AddToHand card] + ) + ] + +-- Look mom! Tavern spells can be modeled as a minion. But a Spell type is absolutely needed in later versions enchantedLasso :: Card -enchantedLasso = Card EnchantedLasso (Stats 0 0) [Battlecry (GetRandom [RTarget ShopRandom])] +enchantedLasso = + Card + EnchantedLasso + (Stats 0 0) + SpellTODO + [ Keyword $ + Battlecry + ( do + ci <- targetRandomCard Shop [IsMinion] + either (return . Left) (\ci -> return $ Right [Take ci]) ci + ) + ] misfitDragonling :: Card -misfitDragonling = Card MisfitDragonling (Stats 2 1) [StartOfCombat [GainStats]] - -data GameState - = GameState - { p1 :: [Card], - p2 :: [Card], - onSummonCallbacks :: [GameState -> GameState], - onPlayCallbacks :: [GameState -> GameState] - } - -trade :: (CardInstance, CardInstance) -> (CardInstance, CardInstance) -trade (CardInstance (Card name1 stats1 fns1), CardInstance (Card name2 stats2 fns2)) = (_, _) - where - ds1 = DivineShield `elem` fns1 \ No newline at end of file +misfitDragonling = + Card + MisfitDragonling + (Stats 2 1) + Dragon + [ Keyword $ + StartOfCombat + ( do + t <- queryTier + return $ Right [GainTempStats (Stats t t)] + ) + ] + +anglersLure :: Card +anglersLure = + Card + AnglersLure + (Stats 0 0) + SpellTODO + [ Keyword $ + Battlecry + ( return $ + Right + [ GainTempStats (Stats 0 2), + GainTaunt + ] + ) + ] + +deepseaAngler :: Card +deepseaAngler = + Card + DeepseaAngler + (Stats 2 2) + Naga + [ Keyword $ Spellcraft anglersLure + ] + +moltenRock :: Card +moltenRock = Card MoltenRock (Stats 3 3) Elemental [Event $ AfterPlay (\card -> tribe card == Elemental) (return $ Right [GainPermStats (Stats 0 1)])] + +pickyEater :: Card +pickyEater = + Card + PickyEater + (Stats 1 1) + Demon + [ Keyword $ + Battlecry + ( do + toEat <- targetRandomCard Shop [IsMinion] -- pickEater's battlecry should fail if there is nothing to eat! + either (return . Left) (\ci -> return $ Right [RemoveFromShop ci, GainPermStats (stats (card ci))]) toEat + ) + ] + +snailCavalry :: Card +snailCavalry = + Card + SnailCavalry + (Stats 2 2) + Naga + [ Combinator $ + UpTo + 1 + PerRecruit + ( AfterPlay (\c -> tribe c == SpellTODO) (return $ Right [GainPermStats (Stats 1 1)]) + ) + ] \ No newline at end of file