Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

On Haskell, what is the linguistic way to represent a card effect for a card game?

I have a simple one-player Card Game:

data Player = Player {
    _hand :: [Card],
    _deck :: [Card],
    _board :: [Card]}
$(makeLenses ''Player)

Some cards have an effect. For example, "Erk" is a card with the following effect:

Flip a coin. If heads, shuffle your deck.

I've implemented it as such:

shuffleDeck :: (MonadRandom m, Functor m) => Player -> m Player
shuffleDeck = deck shuffleM

randomCoin :: (MonadRandom m) => m Coin
randomCoin = getRandom

flipCoin :: (MonadRandom m) => m a -> m a -> m a
flipCoin head tail = randomCoin >>= branch where
    branch Head = head
    branch Tail = tail

-- Flip a coin. If heads, shuffle your deck.
erk :: (MonadRandom m, Functor m) => Player -> m Player
erk player = flipCoin (deck shuffleM player) (return player)

While this certainly does the job, I find an issue on the forced coupling to the Random library. What if I later on have a card that depends on another monad? Then I'd have to rewrite the definition of every card defined so far (so they have the same type). I'd prefer a way to describe the logic of my game entirely independent from the Random (and any other). Something like that:

erk :: CardAction
erk = do
    coin <- flipCoin
    case coin of
        Head -> shuffleDeck
        Tail -> doNothing

I could, later on, have a runGame function that does the connection.

runGame :: (RandomGen g) => g -> CardAction -> Player -> Player

I'm not sure that would help. What is the correct, linguistic way to deal with this pattern?

like image 674
MaiaVictor Avatar asked Mar 09 '15 06:03

MaiaVictor


1 Answers

This is one of the engineering problems the mtl library was designed to solve. It looks like you're already using it, but don't realize its full potential.

The idea is to make monad transformer stacks easier to work with using typeclasses. A problem with normal monad transformer stacks is that you have to know all of the transformers you're using when you write a function, and changing the stack of transformers changes how lifts work. mtl solves this by defining a typeclass for each transformer it has. This lets you write functions that have a class constraint for each transformer it requires but can work on any stack of transformers that includes at least those.

This means that you can freely write functions with different sets of constraints and then use them with your game monad, as long as you game monad has at least those capabilities.

For example, you could have

erk  :: MonadRandom m => ...
incr :: MonadState GameState m => ...
err  :: MonadError GameError m => ...
lots :: (MonadRandom m, MonadState GameState m) => ...

and define your Game a type to support all of those:

type Game a = forall g. RandT g (StateT GameState (ErrorT GameError IO)) a

You'd be able to use all of these interchangeably within Game, because Game belongs to all of those typeclasses. Moreover, you wouldn't have to change anything except the definition of Game if you wanted to add more capabilities.

There's one important limitation to keep in mind: you can only access one instance of each transformer. This means that you can only have one StateT and one ErrorT in your whole stack. This is why StateT uses a custom GameState type: you can just put all of the different things you may want to store throughout your game into that one type so that you only need one StateT. (GameError does the same for ErrorT.)

For code like this, you can get away with just using the Game type directly when you define your functions:

flipCoin :: Game a -> Game a -> Game a
flipCoin a b = ...

Since getRandom has a type polymorphic over m itself, it will work with whatever Game happens to be as long as it has at least a RandT (or something equivalent) inside.

So, to answer you question, you can just rely on the existing mtl typeclasses to take care of this. All of the primitive operations like getRandom are polymorphic over their monad, so they will work with whatever stack you end up with in the end. Just wrap all your transformers into a type of your own (Game), and you're all set.

like image 183
Tikhon Jelvis Avatar answered Sep 28 '22 15:09

Tikhon Jelvis