在Haskell上,用什么语言方式来表示纸牌游戏的牌效应?

Mai*_*tor 5 dsl haskell design-patterns functional-programming

我有一个简单的单人纸牌游戏:

data Player = Player {
    _hand :: [Card],
    _deck :: [Card],
    _board :: [Card]}
$(makeLenses ''Player)
Run Code Online (Sandbox Code Playgroud)

有些卡片有效果.例如,"Erk"是具有以下效果的卡片:

Flip a coin. If heads, shuffle your deck.
Run Code Online (Sandbox Code Playgroud)

我已经实现了它:

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)
Run Code Online (Sandbox Code Playgroud)

虽然这确实起到了作用,但我发现强制耦合到Random库的问题.如果我以后有一张取决于另一个monad的卡怎么办?然后我必须重写到目前为止定义的每张卡的定义(所以它们具有相同的类型).我更喜欢用一种方式来描述我的游戏逻辑完全独立于Random(和任何其他).像这样的东西:

erk :: CardAction
erk = do
    coin <- flipCoin
    case coin of
        Head -> shuffleDeck
        Tail -> doNothing
Run Code Online (Sandbox Code Playgroud)

我可以,稍后,有一个runGame功能,可以进行连接.

runGame :: (RandomGen g) => g -> CardAction -> Player -> Player
Run Code Online (Sandbox Code Playgroud)

我不确定这会有所帮助.处理这种模式的正确语言方法是什么?

Tik*_*vis 5

这是mtl图书馆旨在解决的工程问题之一.看起来你已经在使用它,但没有意识到它的全部潜力.

我们的想法是使monad变换器堆栈更容易使用类型类.普通monad变换器堆栈的一个问题是,你必须知道在编写函数时所使用的所有变换器,并且更改变换器堆栈会改变升降机的工作方式.mtl通过为每个变换器定义一个类型类来解决这个问题.这使您可以编写对其所需的每个变换器具有类约束的函数,但可以在包含至少包含这些变换器的任何变换器堆栈上工作.

这意味着您可以自由地使用不同的约束集编写函数,然后将它们与游戏monad一起使用,只要游戏monad 至少具有这些功能即可.

例如,你可以拥有

erk  :: MonadRandom m => ...
incr :: MonadState GameState m => ...
err  :: MonadError GameError m => ...
lots :: (MonadRandom m, MonadState GameState m) => ...
Run Code Online (Sandbox Code Playgroud)

并定义您的Game a类型以支持所有这些:

type Game a = forall g. RandT g (StateT GameState (ErrorT GameError IO)) a
Run Code Online (Sandbox Code Playgroud)

您可以在其中交替使用所有这些Game,因为Game属于所有这些类型类.此外,除了Game要添加更多功能的定义之外,您不必更改任何内容.

要记住一个重要的限制:您只能访问每个变换器的一个实例.这意味着您只能在整个堆栈中拥有一个StateT和一个ErrorT.这就是StateT使用自定义GameState类型的原因:您可以将整个游戏中可能要存储的所有不同内容放入该类型中,这样您只需要一个类型StateT.(GameError也是如此ErrorT.)

对于这样的代码,您可以Game在定义函数时直接使用类型:

flipCoin :: Game a -> Game a -> Game a
flipCoin a b = ...
Run Code Online (Sandbox Code Playgroud)

因为它本身getRandom具有类型多态性m,所以Game只要它内部至少有一个RandT(或类似的东西)就可以使用它.

所以,为了回答你的问题,你可以依靠现有的mtl类型来处理这个问题.所有的原始操作getRandom都是monad的多态,所以它们最终会与你最终的堆栈一起工作.只需将所有变换器包装成您自己的类型(Game),就可以了.