我正在尝试在Haskell中编写一个双人游戏,比如跳棋。我设想有类型GameState
,Move
和定义游戏规则的函数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]
一个monadic函数,它接收玩家和初始状态,并返回懒惰的动作列表。但是最重要的是,我想要一个基于文本的界面,比如说,允许首先从一系列可能性中选择每个玩家,然后让游戏进行游戏。所以我需要:
playGame :: IO ()
我无法看到如何以通用方式定义给定moveList的playGame。或者我的总体方法不对吗?
编辑:进一步思考它,我甚至没有看到如何定义上面的moveList。例如,如果玩家1是人类,那么IO和玩家2是有状态AI,那么状态,玩家1的第一步将具有类型IO Move
。然后,玩家2必须采用IO GameState
类型的结果状态并产生State IO Move
类型的移动,而玩家1的下一步移动将是IO State IO Move
类型的移动?这看起来不对。
答案 0 :(得分:11)
这个问题分为两部分:
您使用生成器解决了前一个问题,这是一个免费的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
是一个monad变换器(因为FreeT f
是一个monad变换器,当f
是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
...
我们可以使用Identity
monad作为基础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
包已经定义了这些(免责声明:我写了它并且爱德华合并了它的功能被合并到free
包中。我只保留它用于教学目的,你应该使用{{1}如果可能的话。)
有了这些,我们可以定义纯国际象棋移动生成器:
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'
现在,回答你的下一个问题,即如何在运行时选择基本monad。这很简单:
>>> purePlayerToList purePlayer
[Check, CheckMate]
现在,这里的事情变得棘手。你实际上想要两个玩家,你想要为它们两个独立指定基础monad。要做到这一点,你需要一种方法从每个玩家中检索一个移动作为main = do
putStrLn "Pick a monad!"
whichMonad <- getLine
case whichMonad of
"IO" -> runIOPlayer ioPlayer
"Pure" -> purePlayerToIO purePlayer
"Purer!" -> print $ purePlayerToList purePlayer
monad中的动作,并保存玩家移动列表的其余部分以供日后使用:
IO
step
:: GeneratorT ChessMove m r
-> IO (Either r (ChessMove, GeneratorT ChessMove m r))
部分是为了防止玩家用尽了移动(即到达他们的monad的末尾),在这种情况下Either r
是该块的返回值。
此函数特定于每个monad r
,因此我们可以输入class it:
m
让我们定义一些实例:
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
我希望有所帮助。这可能有点过于杀戮(你可能会使用比生成器更简单的东西),但我想在你设计你的游戏时提供一些很酷的Haskell成语。我输入了除最后几个代码块以外的所有代码块,因为我无法想出一个合理的游戏逻辑来进行动态测试。
如果这些示例不够用,您可以详细了解free monads和free monad transformers。
答案 1 :(得分:6)
我的建议有两个主要部分:
对于第一部分,我的意思是你应该考虑创建一个像
这样的数据类型data Player m = Player { selectMove :: m Move }
-- or even
type Player m = m Move
第二部分的含义是使用MonadIO
和MonadState
等类来保持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
。