I developed a card game engine for a german three player card game called Skat. The operating code lives in a StateT SkatEnv (WriterT [Trick] IO) Monad.

data SkatEnv = SkatEnv { piles :: Piles
                       , turnColour :: Maybe TurnColour
                       , skatGame :: Game
                       , players :: Players
                       , currentHand :: Hand
                       , skatSinglePlayer :: Hand }
               deriving Show

type Skat = StateT SkatEnv (WriterT [Trick] IO)

To force the three player character on the typelevel, I decided to use a special Players datatype instead of a list, that uses an existentially quantified type PL that can hold any kind of player, that implements the Player typeclass. To make match specific information available to the Player, the chooseCard etc. functions live in a MonadPlayer Monad.

class (Monad m, MonadIO m) => MonadPlayer m where
    trump :: m Trump
    turnColour :: m (Maybe TurnColour)
    singlePlayer :: m Hand
    game :: m Game

class (Monad m, MonadIO m, MonadPlayer m) => MonadPlayerOpen m where
    showPiles :: m (Piles)

class Player p where
    team :: p -> Team
    hand :: p -> Hand
    chooseCard :: (HasCard d, HasCard c, MonadPlayer m)
               => p
               -> [CardS Played]
               -> [CardS Played]
               -> Maybe [d]
               -> [c]
               -> m (Card, p)
    onCardPlayed :: MonadPlayer m
                 => p
                 -> CardS Played
                 -> m p
    chooseCardOpen :: MonadPlayerOpen m
                   => p
                   -> m Card

data PL = forall p. (Show p, Player p) => PL p

data Players = Players PL PL PL
               deriving Show

By using the MonadPlayer interface I can use my Skat monad for calling the Player's functions without exposing unallowed information, e.g. the other players' cards.

instance MonadPlayer Skat where
    trump = getTrump <$> P.game
    turnColour = gets turnColour
    singlePlayer = gets skatSinglePlayer
    game = gets skatGame

I used the PL wrapper, to be able to implement different players in seperate modules, to keep the code clean and extendible, since I have many different players, e.g. an online player that communicates via a socket or a bot, that heuristically decides which card to play.

Another concern for me is the need of the MonadIO restriction for the MonadPlayer typeclass. This is needed, because my online player for example needs to send messages via a socket, which obviously needs to live in IO, although the bot player could perfectly deal without it. But since I have one common Player interface, I have to make IO available to every player.

Is it better style, not to use the PL wrapper but implement the Players type as

data Players a b c = Players a b c

This would lead to type variables all over the place, especially for everything SkatEnv related.

I left out many implementation details, e.g. many small type definitions. Please let me know, if you need more details about a specific type.


Can you make Player a datatype instead of a typeclass? The datatype can have exactly the same methods but with the first argument elided. Make Player an instance of Show, and voilá, data Players = Players Player Player Player. One implications is that a bot and a human player are now both just values of the same type, Player. Thus the chooseCard, chooseCardOpen and onCardPlayed of a bot can yield a human with a hand and cards on the table mid-game, and vice versa. I'd love to have the freedom to have a bot play the first few rounds for me, yield to me when the game starts going, and play out the last couple of rounds when I feel victory is secure! And as you note, you already opted to give every player type MonadIO, anyway, so these types are not really looser.

  • \$\begingroup\$ This is actually a pretty good idea, although I'm afraid it does not work for me, because my different player types should be able to store additional data they need, to process chooseCard etc. I would have to add these data fields to the general Player data type, which does not fit the idea of an abstract player interface, that is independent of the implementation. \$\endgroup\$ – Erich Apr 14 '20 at 8:44
  • \$\begingroup\$ Can you parameterize Player by the type of the additional information (or the unit type if none) and add one field of that type to Player? Failing that, is this level of extensibility needed (it could well be if you're writing a library!), or can Player be a sum type and Human, Bot and so on be alternate constructors instead of instances? \$\endgroup\$ – Bjartur Thorlacius Apr 15 '20 at 13:09
  • \$\begingroup\$ It is supposed to be a library, so I really need the extensibility. Parameterizing Player is an option, but I feel like it is not necessarily a style improvement over what I currently have :/ How do you feel about this? \$\endgroup\$ – Erich Apr 15 '20 at 13:48
  • \$\begingroup\$ I feel like explicit record passing would be a marginal improvement, hopefully ergonomically but IMO also conceptually. It means that any function that accepts one kind of a player as an argument must be able to handle any of them. If that's unreasonable, then I recommend keeping the typeclass. Since you're writing a library, you're not only making that trade-off between freedom and constraint for yourself but also for code that uses the library. \$\endgroup\$ – Bjartur Thorlacius Apr 20 '20 at 19:05
  • \$\begingroup\$ I can't foresee how your library is going to be used so I can't make the final call either way. Nor can I rid you of constraints except by recommending requiring concrete types upfront. I find the interplay between the Ord typeclass and Set from containres enlightening in this regard. Ord happens to be a typeclass, meaning that there is a canonical order for many types. That means Set can indulge in being a type. But then again, most interesting operations on Set state an Ord constraint. But I digress, this is not directly comparable to your situation. \$\endgroup\$ – Bjartur Thorlacius Apr 20 '20 at 19:12

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.