11

チェッカーなど、Haskell で 2 人用のゲームを作成しようとしています。ゲームのルールを定義する型GameStateMove、および関数を持つことを想定しています。result :: GameState -> Move -> GameState人間と自動化されたプレーヤーの両方が必要であり、型クラスを使用してこれを行うことにしました。

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

ここで、m は基本的な AI プレーヤーの Identity、人間の IO、移動間で状態を維持する AI の State などになる可能性があります。問題は、これらからゲーム ループ全体にどのように移行するかです。次のように定義できると思います。

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 の状態を取得して type の手IO GameStateを生成する必要がありState IO Move、プレーヤー 1 の次の動きは type IO State IO Move?になります。それは正しくありません。

4

2 に答える 2

11

この質問には2つの部分があります。

  • モナドに依存しないチェスプレイフレームワークとインクリメンタルモナド固有の入力を組み合わせる方法
  • 実行時にモナド固有の部分を指定する方法

前者の問題は、フリーモナド変換子の特殊なケースであるジェネレーターを使用して解決します。

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ですFunctor)。yieldこれは、を使用してベースモナドを呼び出すことにより、(ベースモナドでは多型である)モナド固有の呼び出しと混合できることを意味しliftます。

この例のために、いくつかの偽のチェスの動きを定義します。

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

簡単でした!を使用して、一度に1ムーブずつ結果をアンラップでき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モナドをベースモナドとして使用して同じことを行うことができます。

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

さて、ここで物事がトリッキーになります。実際には2人のプレーヤーが必要であり、両方のベースモナドを個別に指定する必要があります。IOこれを行うには、モナドのアクションとして各プレーヤーから1つの動きを取得し、後で使用するためにプレーヤーの残りの動きリストを保存する方法が必要です。

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

このEither r部分は、プレーヤーが移動を使い果たした場合(つまり、モナドの終わりに達した場合)です。この場合、rはブロックの戻り値です。

この関数は各モナドに固有であるため、classitmと入力できます。

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イディオムの一般的なツアーを提供したいと思いました。その場でテストするための賢明なゲームロジックを思い付くことができなかったため、最後のいくつかのコードブロックを除くすべてをタイプチェックしました。

これらの例では不十分な場合は、無料のモナド無料のモナド変換子について詳しく知ることができます。

于 2012-10-14T03:03:07.543 に答える
6

私のアドバイスには、主に 2 つの部分があります。

  1. 新しい型クラスの定義をスキップします。
  2. 既存の型クラスによって定義されたインターフェイスにプログラムします。

最初の部分では、次のようなデータ型を作成することを検討する必要があるということです

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

2 番目の部分が意味することは、ようなクラスを使用し、値をポリモーフィックMonadIOMonadState保ち、Playerすべてのプレーヤーを組み合わせた後にのみ適切なモナド インスタンスを選択することです。たとえば、

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

おそらく、あなたが望む他のプレーヤーも見つかるでしょう。とにかく、これの要点は、これらすべてのプレーヤを作成したら、それらが上記のように型クラス ポリモーフィックである場合、必要なすべてのクラスを実装する特定のモナドを選択することができ、それで完了です。たとえば、これら 3 つの場合は、 を選択できますReaderT GameState IO

幸運を!

于 2012-10-14T00:19:13.723 に答える