目前,我尝试编写一个小型游戏程序(Skat)作为业余爱好项目。斯卡特是一个招人游戏,两名球员对阵一名球员。由于有不同类型的玩家(lokal播放器,网络播放器,计算机等),我想将界面抽象给玩家。
我的基本想法是使用类型类Player
,它定义所有类型的事物,玩家必须做和知道(玩牌,获得有关谁赢得技巧的通知等)。然后,整个游戏只是由函数playSkat :: (Player a, Player b, Player c) => a -> b -> c -> IO ()
完成,其中a
,b
和c
可能是不同类型的玩家。然后,玩家可以以实现定义的方式做出反应。 lokal播放器会在终端上收到一些消息,网络播放器可能会通过网络发送一些信息,而计算机播放器可能会计算出新策略。
因为玩家可能想要做一些IO并且肯定想要某种状态来跟踪私人事物,所以它必须存在于某种Monad中。所以我考虑像这样定义Player
类:
class Player p where
playCard :: [Card] -> p -> IO (Card,p)
notifyFoo :: Event -> p -> IO p
...
这种模式似乎与状态转换器非常相似,但我不知道如何处理它。如果我把它作为额外的monad-transformer写在IO之上,那么我在一天结束时会有三个不同的monad。如何以良好的方式编写这种抽象?
为了澄清,我需要的是,通常的控制流程应该是这样的:
玩耍时,第一个玩家会玩牌,然后是第二个,最后是第三个。为此,逻辑需要为每个玩家执行函数playCard
。之后,逻辑决定哪个玩家赢得了技巧并将获胜的信息发送给所有玩家。
答案 0 :(得分:6)
首先,请记住,类型类的主要目的是允许重载函数,即您可以使用不同类型的单个函数。你真的不需要那样,所以你最好用
的记录类型data Player = Player { playCard :: [Card] -> IO (Card, Player), ... }
其次,一些玩家需要IO的问题和一些不能用自定义monad解决的问题。我为TicTacToe游戏编写了相应的example code,这是我operational包的一部分。
答案 1 :(得分:4)
更好的设计是不要将IO作为任何播放器类型的一部分。 为什么玩家需要做IO?玩家可能需要获取信息并发送信息。创建一个反映它的界面。如果/当需要IO时,它将由playSkat执行。
如果你这样做,你可以拥有其他版本的playSkat,它们不会做任何IO,你也可以更容易地测试你的玩家,因为他们只通过类方法而不是通过IO进行交互。
答案 2 :(得分:1)
这就是我最终设计抽象的方式:
引擎可能想要的其中一个玩家的所有内容都编码在一个名为Message
的大型GADT中,因为我并不总是需要答案。 GADT的参数是请求的返回值:
data Message answer where
ReceiveHand :: [Card] -> Message ()
RequestBid :: Message (Maybe Int)
HoldsBid :: Int -> Message Bool
...
不同类型的玩家在一个类型类上被抽象,其中一个函数playerMessage
允许引擎向玩家发送消息并请求答案。答案包含在Either
中,因此如果无法返回答案,玩家可以返回相应的错误(例如,如果未执行该功能或网络处于打击状态等)。参数p
是玩家存储私人数据和配置的状态记录。玩家是通过monad m
抽象的,以允许一些玩家使用IO,而其他玩家则不需要它:
class Monad m => Player p m | p -> m where
playerMessage :: Message answer -> p -> m (Either String answer,p)
我问过another Question,因为我不满意一次又一次地输入上下文,所以我最终更改了代码来重新启用类型类Player
。玩家没有自己的状态,但他们可以使用部分应用函数来模拟这个。有关详细信息,请参阅其他问题。
答案 3 :(得分:0)
一点都没想过,但也许还值得考虑。在这里,我注意到您在类型类函数中同时包含p
和p
,我猜这意味着“更新”p
。不知怎的,一个州的monad。
class (MonadIO m, MonadState p m) => Player p where
playCard :: [Card] -> m Card
notifyFoo :: Event -> m ()
同样,这只是一个自发的想法。我不保证它是明智的(甚至是可编辑的)。