Skip to content

Commit

Permalink
Design progress: Free monadizing language.
Browse files Browse the repository at this point in the history
  • Loading branch information
BlastWind committed Oct 20, 2024
1 parent d19f20c commit 16c8e49
Show file tree
Hide file tree
Showing 4 changed files with 213 additions and 87 deletions.
1 change: 1 addition & 0 deletions battlegrounds.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
24 changes: 23 additions & 1 deletion docs/Thoughts.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"?

Expand All @@ -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`.
1 change: 1 addition & 0 deletions package.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ library:
- large-generics == 0.2.2
- large-records == 0.4.1
- record-hasfield
- free == 5.2


executables:
Expand Down
274 changes: 188 additions & 86 deletions src/DesignSketch.hs
Original file line number Diff line number Diff line change
@@ -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)

Expand All @@ -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]

Check warning on line 157 in src/DesignSketch.hs

View workflow job for this annotation

GitHub Actions / build

This binding for ‘card’ shadows the existing binding

Check warning on line 157 in src/DesignSketch.hs

View workflow job for this annotation

GitHub Actions / build

This binding for ‘card’ shadows the existing binding
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

Check warning on line 173 in src/DesignSketch.hs

View workflow job for this annotation

GitHub Actions / build

This binding for ‘ci’ shadows the existing binding

Check warning on line 173 in src/DesignSketch.hs

View workflow job for this annotation

GitHub Actions / build

This binding for ‘ci’ shadows the existing binding
)
]

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
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)])]

Check warning on line 217 in src/DesignSketch.hs

View workflow job for this annotation

GitHub Actions / build

This binding for ‘card’ shadows the existing binding

Check warning on line 217 in src/DesignSketch.hs

View workflow job for this annotation

GitHub Actions / build

This binding for ‘card’ shadows the existing binding

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)])
)
]

0 comments on commit 16c8e49

Please sign in to comment.