11

我正在尝试用 Haskell 编写一个两人游戏,例如跳棋。我设想有 types GameStateMove和一个result :: GameState -> Move -> GameState定义游戏规则的函数。我想拥有人类和自动玩家,我想我会通过一个类型类来做到这一点:

class Player p m | p -> m where
  selectMove :: p -> GameState -> m Move

其中的想法是 m 可以是基本 AI 玩家的身份,人类的 IO,保持跨动作状态的 AI 的状态等。问题是如何从这些到整个游戏循环。我想我可以定义如下:

Player p1 m1, Player p2 m2 => moveList :: p1 -> p2 -> GameState -> m1 m2 [Move]

一个接受玩家和初始状态的单子函数,并返回惰性移动列表。但除此之外,假设我想要一个基于文本的界面,例如,允许首先从可能性列表中选择每个玩家,然后开始玩游戏。所以我需要:

playGame :: IO () 

我看不到如何以通用方式定义给定 moveList 的 playGame 。还是我的整体方法不对?

编辑:进一步考虑,我什至看不到如何在上面定义 moveList。例如,如果玩家 1 是人类,所以是 IO,玩家 2 是有状态的 AI,所以 State,玩家 1 的第一步将是 type IO Move。那么玩家 2 将不得不采用 type 的结果状态IO GameState并产生 type 的移动State IO Move,而玩家 1 的下一步将是 type IO State IO Move?那看起来不对。

4

2 回答 2

11

这个问题有两个部分:

  • 如何将独立于 monad 的国际象棋框架与增量的特定于 monad 的输入混合在一起
  • 如何在运行时指定特定于 monad 的部分

您使用生成器解决了前一个问题,这是一个免费的单子变压器的特例:

import Control.Monad.Trans.Free -- from the "free" package

type GeneratorT a m r = FreeT ((,) a) m r
-- or: type Generator a = FreeT ((,) a)

yield :: (Monad m) => a -> GeneratorT a m ()
yield a = liftF (a, ())

GeneratorT a是一个单子变压器(因为FreeT f是一个单子变压器,免费的,什么时候f是 a Functor)。这意味着我们可以混合yield(在基本 monad 中是多态的),通过使用lift来调用基本 monad 与特定于 monad 的调用。

我将为这个例子定义一些虚假的国际象棋动作:

data ChessMove = EnPassant | Check | CheckMate deriving (Read, Show)

现在,我将定义一个IO基于国际象棋移动的生成器:

import Control.Monad
import Control.Monad.Trans.Class

ioPlayer :: GeneratorT ChessMove IO r
ioPlayer = forever $ do
    lift $ putStrLn "Enter a move:"
    move <- lift readLn
    yield move

那很简单!我们可以使用 一次一个动作解开结果runFreeT,这将只要求玩家在绑定结果时输入一个动作:

runIOPlayer :: GeneratorT ChessMove IO r -> IO r
runIOPlayer p = do
    x <- runFreeT p -- This is when it requests input from the player
    case x of
        Pure r -> return r
        Free (move, p') -> do
            putStrLn "Player entered:"
            print move
            runIOPlayer p'

让我们测试一下:

>>> runIOPlayer ioPlayer
Enter a move:
EnPassant
Player entered:
EnPassant
Enter a move:
Check
Player entered:
Check
...

我们可以使用Identitymonad 作为基础 monad 来做同样的事情:

import Data.Functor.Identity

type Free f r = FreeT f Identity r

runFree :: (Functor f) => Free f r -> FreeF f r (Free f r)
runFree = runIdentity . runFreeT

注意transformers-free包已经定义了这些(免责声明:我编写了它,Edward 合并了它的功能被合并到free包中。我只保留它用于教学目的,如果可能的话你应该使用free)。

有了这些,我们可以定义纯国际象棋移动生成器:

type Generator a r = Free ((,) a) r
-- or type Generator a = Free ((,) a)

purePlayer :: Generator ChessMove ()
purePlayer = do
    yield Check
    yield CheckMate

purePlayerToList :: Generator ChessMove r -> [ChessMove]
purePlayerToList p = case (runFree p) of
    Pure _ -> []
    Free (move, p') -> move:purePlayerToList p'

purePlayerToIO :: Generator ChessMove r -> IO r
purePlayerToIO p = case (runFree p) of
    Pure r -> return r
    Free (move, p') -> do
        putStrLn "Player entered: "
        print move
        purePlayerToIO p'

让我们测试一下:

>>> purePlayerToList purePlayer
[Check, CheckMate]

现在,回答您的下一个问题,即如何在运行时选择基本单子。这很简单:

main = do
    putStrLn "Pick a monad!"
    whichMonad <- getLine
    case whichMonad of
        "IO"     -> runIOPlayer ioPlayer
        "Pure"   -> purePlayerToIO purePlayer
        "Purer!" -> print $ purePlayerToList purePlayer

现在,事情变得棘手了。你实际上想要两个玩家,并且你想独立地为他们指定基本单子。为此,您需要一种方法从每个玩家那里检索一个动作作为IOmonad 中的一个动作,并保存玩家动作列表的其余部分以供以后使用:

step
 :: GeneratorT ChessMove m r
 -> IO (Either r (ChessMove, GeneratorT ChessMove m r))

Either r部分是为了防止玩家用尽所有动作(即到达他们的单子的末尾),在这种情况下,这r是块的返回值。

这个函数是特定于每个 monadm的,所以我们可以输入 class 它:

class Step m where
    step :: GeneratorT ChessMove m r
         -> IO (Either r (ChessMove, GeneratorT ChessMove m r))

让我们定义一些实例:

instance Step IO where
    step p = do
        x <- runFreeT p
        case x of
            Pure r -> return $ Left r
            Free (move, p') -> return $ Right (move, p')

instance Step Identity where
    step p = case (runFree p) of
        Pure r -> return $ Left r
        Free (move, p') -> return $ Right (move, p')

现在,我们可以编写我们的游戏循环,如下所示:

gameLoop
 :: (Step m1, Step m2)
 => GeneratorT ChessMove m1 a
 -> GeneratorT ChessMove m2 b
 -> IO ()
gameLoop p1 p2 = do
    e1 <- step p1
    e2 <- step p2
    case (e1, e2) of
        (Left r1, _) -> <handle running out of moves>
        (_, Left r2) -> <handle running out of moves>
        (Right (move1, p2'), Right (move2, p2')) -> do
            <do something with move1 and move2>
            gameLoop p1' p2'

我们的main函数只是选择要使用的玩家:

main = do
    p1 <- getStrLn
    p2 <- getStrLn
    case (p1, p2) of
        ("IO", "Pure") -> gameLoop ioPlayer purePlayer
        ("IO", "IO"  ) -> gameLoop ioPlayer ioPlayer
        ...

我希望这会有所帮助。这可能有点过头了(你可能可以使用比生成器更简单的东西),但我想对很酷的 Haskell 习语进行总体介绍,你可以在设计游戏时从中取样。我对除了最后几个代码块之外的所有代码块进行了类型检查,因为我无法提出合理的游戏逻辑来进行动态测试。

如果这些示例还不够,您可以了解有关免费 monad免费 monad 转换器的更多信息。

于 2012-10-14T03:03:07.543 回答
6

我的建议有两个主要部分:

  1. 跳过定义新类型类。
  2. 对现有类型类定义的接口进行编程。

对于第一部分,我的意思是你应该考虑创建一个数据类型,比如

data Player m = Player { selectMove :: m Move }
-- or even
type Player m = m Move

第二部分的意思是使用类似的类MonadIOMonadState保持你的Player值是多态的,并且只有在组合所有玩家之后才在最后选择一个合适的 monad 实例。例如,您可能有

computerPlayer :: MonadReader GameState m => Player m
randomPlayer :: MonadRandom m => Player m
humanPlayer :: (MonadIO m, MonadReader GameState m) => Player m

也许你会发现还有其他你想要的球员。无论如何,重点是一旦你创建了所有这些播放器,如果它们是上面的类型类多态,你可以选择一个特定的 monad 来实现所有必需的类,然后你就完成了。例如,对于这三个,您可以选择ReaderT GameState IO.

祝你好运!

于 2012-10-14T00:19:13.723 回答