2012-10-13 53 views
11

我想在Haskell中編寫一個雙人遊戲,比如跳棋。我設想有定義遊戲規則的類型GameState,Move和函數result :: GameState -> Move -> GameState。我想有兩個人工和自動化的球員,我想我會通過具有類型類做到這一點:在運行時選擇一個monad

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() 

我看不出如何以通用的方式定義playGame moveList。或者我的整體方法不對?

編輯:進一步思考它,我什至不知道如何定義moveList上面。例如,如果玩家1是人類,那麼IO和玩家2是有狀態的AI,所以在狀態中,玩家1的第一步將具有類型IO Move。那麼玩家2將不得不採取類型IO GameState的最終狀態併產生State IO Move類型的移動,並且玩家1的下一步移動將是IO State IO Move類型?這看起來不正確。

回答

11

有兩個部分對這個問題:

  • 如何搭配增量具體的單子輸入
  • 單子獨立的下棋框架如何在運行時指定
  • 具體的單子部分

    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變壓器,免費,當fFunctor)。這意味着我們可以通過使用lift來調用基本monad,將yield(在基本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 

這很容易!

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 
... 

我們可以做的,當你綁定的結果我們可以解開使用runFreeT在同一時間,結果一招,這將只要求玩家輸入的舉動使用Identity單子爲基數單子同樣的事情:

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 

NoteThe 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] 

現在,爲了回答你的下一個問題,就是如何選擇基地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)) 

Either r部分的情況下,玩家運行沒有動作(即到達他們的monad的末尾),在這種情況下r是塊的返回值。

此功能是具體到每一個單子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函數只是選擇哪個playe rs使用:

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

我希望有所幫助。這可能有點過分(你可能會使用比發電機更簡單的東西),但我想給大家介紹一下酷炫的Haskell習語,你可以在設計遊戲時進行抽樣。除了最後幾個代碼塊之外,我對所有代碼都進行了類型檢查,因爲我無法想出一個明智的遊戲邏輯來進行即時測試。

如果這些示例不足,您可以瞭解有關free monadsfree monad transformers的更多信息。

+0

謝謝!這似乎是一個不錯的通用解決方案。一個問題:你將如何實施國家monad的步驟? – bhaskarama

+1

我以前有四行,我想「我敢打賭,這是加百列的回答」:) –

+0

@bhaskarama你不會!實際上你可以寫一些類型:'StateT s(Free ChessMove)r',使用'runStateT'將其轉換爲'Free ChessMove r',然後將其作爲「純粹」播放器傳遞。並非所有延伸玩家的monad都屬於基本monad。 –

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

祝你好運!

+0

謝謝。儘管如此,我仍然沒有看到如何編寫整個遊戲循環。也就是說,我如何根據用戶的意見決定每個玩家的類型,然後玩遊戲? – bhaskarama

+0

@bhaskarama在實現了幾個多態玩家之後,您可以選擇一個單一的單形類型,並將它們專門化爲這種類型。遊戲循環也使用單態類型。 –

+0

好的,如果這是一個基本問題,不好意思,但是這個專業怎麼樣?例如,如果有IO,身份的玩家,或者一個有狀態的玩家在移動之間緩存結果,在一個狀態緩存中,另一個有狀態的玩家隨着時間的推移調整它的播放風格。在這種情況下,單一的單形類型會是什麼樣子?如何才能讓玩家專注於這種類型? – bhaskarama