3
\$\begingroup\$

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.

asked Apr 8, 2020 at 9:54
\$\endgroup\$

1 Answer 1

3
\$\begingroup\$

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.

answered Apr 12, 2020 at 23:21
\$\endgroup\$
5
  • \$\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\$ Commented Apr 14, 2020 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\$ Commented Apr 15, 2020 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\$ Commented Apr 15, 2020 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\$ Commented Apr 20, 2020 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\$ Commented Apr 20, 2020 at 19:12

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.