Currently, I try to write a small game program (Skat) as a hobby project. Skat is a trick-taking game were two players play against a single player. As there are different kinds of players (lokal player, network player, computer, etc.), I wanted to abstract the interface to the player.
My basic idea is to use a typeclass Player
, that defines all kind of things, a player have to do and to know (playing a card, get notified about who won the trick, etc). Then, the whole game is just done by a function playSkat :: (Player a, Player b, Player c) => a -> b -> c -> IO ()
where a
, b
and c
might be different kinds of players. A player might then react in a implementation defined way. A lokal player would get some message on his terminal, a network player might send some information over the network and a computer player might calculate a new strategy.
Because the player might want to do some IO and definitly want to have some kind of state to track private things, it has to live in some kind of Monad. So I thought about defining the Player
class like this:
class Player p where
playCard :: [Card] -> p -> IO (Card,p)
notifyFoo :: Event -> p -> IO p
...
This pattern seems to be quite similar to a state transformer, but I am not shure how to handle it. If I would write it as an extra monad-transformer on top of IO, I had three different monads at the end of the day. How can I write this abstraction in a good way?
To clarify, what I need, here is how a usual control flow should look like:
When playing a trick, the first player plays a card, then the second, and finally the third. To do this, the logic needs to execute the function playCard
trice for each player. Afterwards, the logic decides, which player wins the trick and sends the information who won to all players.
First of all, keep in mind that the main purpose of type classes is to permit overloading of functions, i.e. that you can use a single function at different types. You don't really require that, so you are better off with a record type along the lines of
data Player = Player { playCard :: [Card] -> IO (Card, Player), ... }
Second, the problem of some players needing IO and some not can be solved with a custom monad. I have written corresponding example code for a game of TicTacToe, which is part of my operational package.
A much better design would be not to have IO as part of any Player type.
Why does the player need to do IO? The player probably needs to get information and send information. Make an interface that reflects that. If/when IO is needed it will be performed by playSkat.
If you do it this what you can have other versions of playSkat that don't do any IO and you can also test your players much more easily since they only interact via the class methods and not through IO.
That's how I finally designed the abstraction:
All things the engine may want from one of the players are encoded in a big GADT called Message
, because I do not always need an answer. The parameter of the GADT is the requested return value:
data Message answer where
ReceiveHand :: [Card] -> Message ()
RequestBid :: Message (Maybe Int)
HoldsBid :: Int -> Message Bool
...
The different kinds of players are abstracted over a type class with one single function playerMessage
that allows the engine to send a message to a player and requests for an answer. The answer is wrapped up in an Either
, so the player can return an appropriate error if it is not possible to return an answer (for instance, if the function is not implemented or the network is on strike, etc). The parameter p
is a state record for the player to store private data and configuration. The player is abstracted over a monad m
to allow some players to use IO while others don't need it:
class Monad m => Player p m | p -> m where
playerMessage :: Message answer -> p -> m (Either String answer,p)
Edit
I asked another Question, because I was not happy with typing the contexts again and again, so I finally changed the code to reify the typeclass Player
. The players have no state by them self, but they can use partial applied functions to simulate this. See the other question for details.
Haven't at all thought this through, but maybe still worth considering. Here I noticed that you have both p
in and p
out in the type class functions, I guessed that means those "update" p
. A state monad somehow.
class (MonadIO m, MonadState p m) => Player p where
playCard :: [Card] -> m Card
notifyFoo :: Event -> m ()
Again, this is just a spontaneous thought. I don't guarantee it to be wise (or even compilable).