Skip to content

Commit

Permalink
doc updates
Browse files Browse the repository at this point in the history
  • Loading branch information
BlastWind committed Dec 16, 2024
1 parent 83cdc67 commit d8d9d14
Show file tree
Hide file tree
Showing 6 changed files with 420 additions and 40 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@ when `Model.hs` is open.

### Roadmap
1. [x] Basic Game Loop.
2. [ ] Support all Tier 1 cards. The hardest to implement would be Scallywag.
2. [ ] Support the follow cards in this order: A God Card (responds to all events), Harmless Bonehead, Picky Eater, Upbeat Frontdrake, Dune Dweller
3. [ ] Support all cards, completing the single player mode experience.
4. [ ] Server, game rooms, authentication.
37 changes: 37 additions & 0 deletions docs/Thoughts.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,40 @@ Snail Cavalry requires this, but with both `UpTo` and `AfterPlay` being

Resolution:
- Split `Functionalities` into `KeywordFunctionality`, `EventFunctionality`, and `FunctionalityCombinator`.

### Oct 21, 2024:
Question: How to model Brann?

Idea 1: `AfterBattlecryTrigger`. But this would require `AfterBattlecryTrigger` to pass in the `TriggeredBattlecry`, and a new `TriggerBattlecry` state effect.

Idea 2: Reserved `BrannFunctionality`.

Idea 3: Reserved `Trigger Battlecry Times` constructor for `Functionality`.

Idea 4: The game logic keeps an priority queue for how many times do battlecries get triggered,
it's a priority queue because the highest trigger count is used. Brann, upon entering the board,
inserts "twice" into the battlecry priority queue. On leave, it deletes *some* "Battlecry twice" node.

### Oct 26, 2024:
Problem: `retrieveAssociatedCard` is not good correctness by modeling since
a lot of events are not associated cards.

Solution:
#1: For events, it probably makes the most sense to just pass the associated card
#2: To be fancy, we could associate `retrieveAssociatedCard` with a typeclass that
only `EventFunctionalities` get access to. That still seems too much though.

----

Idea: What if I free monadized the design sketch more and delistified the `[Functionality]` that is bound to a `Card`?

----

Idea: What if every Functionality is its own datatype? And, whether or not
it is associated with "keyword", "event", or is a "combinator"
will be described by a `Has` or `Is` style typeclass?

---

Key question: Battlecry effects sometimes have a target, but this target gets
randomized if the effect is to be triggered. How to model this scenario?
137 changes: 118 additions & 19 deletions src/DesignSketch.hs → docs/sketches/DesignSketch.hs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ module DesignSketch (module DesignSketch) where

import Control.Monad.Free

data CardFilterCriterion = MaxTier Int | Tribe Tribe | IsMinion
data CardFilterCriterion = MaxTier Int | Tribe Tribe | IsMinion | NotSelf

data RandomTarget = Hand | Shop | Board

Expand All @@ -14,6 +14,8 @@ data InjectAvatarMethod next
| MakeRandomCard [CardFilterCriterion] (CardInstance -> next)
| TargetRandomCard RandomTarget [CardFilterCriterion] (Either EffectError CardInstance -> next)
| TargetRandomCards RandomTarget [CardFilterCriterion] Int (Either EffectError [CardInstance] -> next)
| RetrieveAssociatedCard (CardInstance -> next)
| RetrieveBoard ([CardInstance] -> next)
deriving (Functor)

type InjectAvatar a = Free InjectAvatarMethod a
Expand All @@ -30,13 +32,24 @@ targetRandomCard randTarget crits = liftF $ TargetRandomCard randTarget crits id
targetRandomCards :: RandomTarget -> [CardFilterCriterion] -> Int -> InjectAvatar (Either EffectError [CardInstance])
targetRandomCards randomTarget crits count = liftF $ TargetRandomCards randomTarget crits count id

retrieveAssociatedCard :: InjectAvatar CardInstance
retrieveAssociatedCard = liftF $ RetrieveAssociatedCard id

retrieveBoard :: InjectAvatar [CardInstance]
retrieveBoard = liftF $ RetrieveBoard id

data StateEffect
= -- E.g., Trusty Pup
= -- Stats that will be permanently gained even during combat. E.g., Trusty Pup
GainPermStats Stats
| -- E.g., Stats gained during Combat (Glim Guardian); Spellcraft
| -- Stats that will be permanent if gained during recruit, and temporary if gained during combat. E.g., Blazing Skyfin
GainStats Stats
| -- Stats that is only temporarily gained no matter what. E.g., Spellcraft.
GainTempStats Stats
| -- E.g., Ancestral Automaton, Eternal Knight, (Maybe) Deep Blue
GainBaseStats Stats
| -- Stats that is permanently gained for all future instances. E.g., Ancestral Automaton, Eternal Knight
GainStatsForAll Stats
| -- Reserved for deep blue. TODO: Is there a way to factor this into the current gain stat schemes?
GainStatsDeepBlue Stats
| GainTempTaunt
| GainTaunt
| -- E.g., Cord Puller
Summon CardName
Expand All @@ -49,8 +62,10 @@ data StateEffect
| Take CardInstance
| -- E.g., Tavern Coin
GainGold Int
| -- E.g., Brann, Dreamer's Embrace
TriggerBattlecry CardInstance

data Tribe = Murloc | Dragon | Demon | Elemental | Undead | Mech | Naga | SpellTODO deriving (Eq)
data Tribe = Murloc | Dragon | Demon | Elemental | Undead | Mech | Naga | MurlocDragon | All | SpellTODO deriving (Eq)

data CounterType
= -- Upbeat Frontdrake
Expand Down Expand Up @@ -80,15 +95,16 @@ data KeywordFunctionality
| Spellcraft Card

data EventFunctionality
= -- Events detectable by "inspecting" the card itself
= -- Self 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
| -- Global events: Detectable by listening onto some other more "global" events
AfterPlay (InjectAvatar (Either EffectError [StateEffect]))
| AfterSummon (InjectAvatar (Either EffectError [StateEffect]))
| AfterBattlecryTrigger (InjectAvatar (Either EffectError [StateEffect])) -- due to cards like Rylak, Dreamer's Embrace, this needs its own event.
| -- Every `count` times `counterType` happens, run effects. `counterType` are more global events.
Every Count CounterType (InjectAvatar (Either EffectError [StateEffect]))

data FunctionalityCombinator
Expand Down Expand Up @@ -116,6 +132,10 @@ data CardName
| DeepseaAngler
| AnglersLure
| SnailCavalry
| RecruitATrainee
| BlazingSkyfin
| AncestralAutomaton
| BrannBronzebeard
deriving (Eq)

data Card = Card
Expand All @@ -125,10 +145,13 @@ data Card = Card
functionality :: [Functionality]
}

newtype CardInstance = CardInstance {card :: Card}
data CardInstance = CardInstance {card :: Card, instanceId :: Int}

instance Eq CardInstance where
a == b = instanceId a == instanceId b

glimGuardian :: Card
glimGuardian = Card GlimGuardian (Stats 1 4) Dragon [Event $ OnAttack (return $ Right [GainTempStats (Stats 2 1)])]
glimGuardian = Card GlimGuardian (Stats 1 4) Dragon [Event $ OnAttack (return $ Right [GainStats (Stats 2 1)])]

skeleton :: Card
skeleton = Card Skeleton (Stats 1 1) Undead []
Expand Down Expand Up @@ -184,7 +207,7 @@ misfitDragonling =
StartOfCombat
( do
t <- queryTier
return $ Right [GainTempStats (Stats t t)]
return $ Right [GainStats (Stats t t)]
)
]

Expand All @@ -199,7 +222,7 @@ anglersLure =
( return $
Right
[ GainTempStats (Stats 0 2),
GainTaunt
GainTempTaunt
]
)
]
Expand All @@ -214,7 +237,18 @@ deepseaAngler =
]

moltenRock :: Card
moltenRock = Card MoltenRock (Stats 3 3) Elemental [Event $ AfterPlay (\card -> tribe card == Elemental) (return $ Right [GainPermStats (Stats 0 1)])]
moltenRock =
Card
MoltenRock
(Stats 3 3)
Elemental
[ Event $
AfterPlay
( do
c <- retrieveAssociatedCard
return $ Right [GainStats (Stats 0 1) | (tribe . card) c == Elemental]
)
]

pickyEater :: Card
pickyEater =
Expand All @@ -226,7 +260,7 @@ pickyEater =
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
either (return . Left) (\ci -> return $ Right [RemoveFromShop ci, GainStats (stats (card ci))]) toEat
)
]

Expand All @@ -240,6 +274,71 @@ snailCavalry =
UpTo
1
PerRecruit
( AfterPlay (\c -> tribe c == SpellTODO) (return $ Right [GainPermStats (Stats 1 1)])
( AfterPlay
( do
c <- retrieveAssociatedCard
return $ Right [GainStats (Stats 1 1) | (tribe . card) c == SpellTODO]
)
)
]
]

recruitATrainee :: Card
recruitATrainee =
Card
RecruitATrainee
(Stats 0 0)
SpellTODO
[ Keyword $
Battlecry
( do
c <- makeRandomCard [MaxTier 1]
return $ Right [AddToHand c]
)
]

blazingSkyfin :: Card
blazingSkyfin =
Card
BlazingSkyfin
(Stats 2 4)
MurlocDragon
[ Event $
AfterBattlecryTrigger (return $ Right [GainStats (Stats 1 1)])
]

ancestralAutomaton :: Card
ancestralAutomaton =
Card
AncestralAutomaton
(Stats 2 5)
Mech
[ Event $
AfterSummon
( do
c <- retrieveAssociatedCard
return $ Right [GainPermStats (Stats 2 1) | cardName (card c) == AncestralAutomaton]
)
]

data Free' t a
= Pure' a -- Termination case
| Free' -- Recursive nesting of language, store an outer monadic action that keeps another continuation action inside.
( t -- Algebra of the language
(Free' t a) -- Nested language in the free form.
)

-- brannBronzebeard :: Card
-- brannBronzebeard =
-- Card
-- BrannBronzebeard
-- (Stats 2 4)
-- Neutral
-- [ Event $
-- AfterBattlecryTrigger
-- ( do
-- c <- retrieveAssociatedCard
-- b <- retrieveBoard
-- -- Only do brann stuff if this is the first brann. Brann effects do not stack.
-- return _
-- )
-- ]
71 changes: 51 additions & 20 deletions src/DesignSketch2.hs → docs/sketches/DesignSketch2.hs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,19 @@
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE RankNTypes #-}

{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE TypeFamilies #-}

module DesignSketch2 where

import Control.Monad.Except
import Control.Monad.Random
import Control.Monad.Reader
import Effectful
import Effectful.Error.Static
import Effectful.Dispatch.Dynamic

data Tribe = Murloc | Dragon | Demon | Elemental | Undead | Mech | Naga | MurlocDragon | All | SpellTODO deriving (Eq)

Expand Down Expand Up @@ -49,6 +58,7 @@ data CardName
| BlazingSkyfin
| AncestralAutomaton
| BrannBronzebeard
| DummyCard
deriving (Eq)

data StateEffect
Expand Down Expand Up @@ -83,22 +93,22 @@ data KeywordFunctionality
| DivineShield
| Reborn
| Windfury
| Deathrattle (forall m. (Avatar m) => m [StateEffect])
| StartOfCombat (forall m. (Avatar m) => m [StateEffect])
| Battlecry (forall m. (Avatar m) => m [StateEffect])
| Deathrattle (forall es. (Avatar :> es) => Eff es [StateEffect])
| StartOfCombat (forall es. (Avatar :> es) => Eff es [StateEffect])
| Battlecry (forall es. (Avatar :> es) => Eff es [StateEffect])
| Spellcraft Card

type Count = Int

data EventFunctionality
= OnAttack (forall m. (Avatar m) => m [StateEffect])
| OnDamaged (forall m. (Avatar m) => m [StateEffect])
| OnKill (forall m. (Avatar m) => m [StateEffect])
| OnSell (forall m. (Avatar m) => m [StateEffect])
| AfterPlay (forall m. (Avatar m) => m [StateEffect])
| AfterSummon (forall m. (Avatar m) => m [StateEffect])
| AfterBattlecryTrigger (forall m. (Avatar m) => m [StateEffect])
| Every Count CounterType (forall m. (Avatar m) => m [StateEffect])
= OnAttack (forall es. (Avatar :> es) => Eff es [StateEffect])
| OnDamaged (forall es. (Avatar :> es) => Eff es [StateEffect])
| OnKill (forall es. (Avatar :> es) => Eff es [StateEffect])
| OnSell (forall es. (Avatar :> es) => Eff es [StateEffect])
| AfterPlay (forall es. (Avatar :> es) => Eff es [StateEffect])
| AfterSummon (forall es. (Avatar :> es) => Eff es [StateEffect])
| AfterBattlecryTrigger (forall es. (Avatar :> es) => Eff es [StateEffect])
| Every Count CounterType (forall es. (Avatar :> es) => Eff es [StateEffect])

data FunctionalityCombinator
= -- Per `Per`, run effect up to `Count` times.
Expand All @@ -118,13 +128,34 @@ data Card = Card

data CardInstance = CardInstance {card :: Card, instanceId :: Int}

class (Monad m, MonadError EffectError m) => Avatar m where
queryTier :: m Int
makeRandomCard :: [CardFilterCriterion] -> m CardInstance
targetRandomCard :: RandomTarget -> [CardFilterCriterion] -> m CardInstance
targetRandomCards :: RandomTarget -> [CardFilterCriterion] -> Int -> m [CardInstance]
retrieveAssociatedCard :: m CardInstance
retrieveBoard :: m [CardInstance]
data Avatar :: Effect where
QueryTier :: Avatar m Int
MakeRandomCard :: [CardFilterCriterion] -> Avatar m CardInstance
TargetRandomCard :: RandomTarget -> [CardFilterCriterion] -> Avatar m CardInstance
TargetRandomCards :: RandomTarget -> [CardFilterCriterion] -> Int -> Avatar m [CardInstance]
RetrieveAssociatedCard :: Avatar m CardInstance
RetrieveBoard :: Avatar m [CardInstance]

type instance DispatchOf Avatar = Dynamic


queryTier :: Avatar :> es => Eff es Int
queryTier = send QueryTier

makeRandomCard :: Avatar :> es => [CardFilterCriterion] -> Eff es CardInstance
makeRandomCard = send . MakeRandomCard

targetRandomCard :: Avatar :> es => RandomTarget -> [CardFilterCriterion] -> Eff es CardInstance
targetRandomCard target = send . TargetRandomCard target

targetRandomCards :: Avatar :> es => RandomTarget -> [CardFilterCriterion] -> Int -> Eff es [CardInstance]
targetRandomCards target criteria = send . TargetRandomCards target criteria

retrieveAssociatedCard :: Avatar :> es => Eff es CardInstance
retrieveAssociatedCard = send RetrieveAssociatedCard

retrieveBoard :: Avatar :> es => Eff es [CardInstance]
retrieveBoard = send RetrieveBoard

data GameState = GameState {}

Expand Down
Loading

0 comments on commit d8d9d14

Please sign in to comment.