7

現在、趣味のプロジェクトとして小さなゲーム プログラム (Skat) を作成しようとしています。Skat は、2 人のプレイヤーが 1 人のプレイヤーと対戦するトリックテイキング ゲームです。プレーヤーにはさまざまな種類 (ローカル プレーヤー、ネットワーク プレーヤー、コンピューターなど) があるため、プレーヤーへのインターフェイスを抽象化したいと考えました。

私の基本的なアイデアは、 typeclass を使用することですPlayer。これは、プレイヤーがしなければならないことや知っておくべきことすべてを定義します (カードをプレイする、誰がトリックに勝ったかについて通知を受けるなど)。次に、ゲーム全体playSkat :: (Player a, Player b, Player c) => a -> b -> c -> IO ()a、 、bおよびcさまざまな種類のプレーヤーである関数によって行われます。プレーヤーは、実装で定義された方法で反応する場合があります。ローカル プレイヤーは自分の端末で何らかのメッセージを受信し、ネットワーク プレイヤーはネットワークを介して何らかの情報を送信し、コンピューター プレイヤーは新しい戦略を計算します。

プレイヤーは何らかの IO を実行したい場合があり、プライベートなものを追跡するためにある種の状態を確実に持ちたい場合があるため、ある種のモナドに存在する必要があります。Playerそこで、次のようにクラスを定義することを考えました。

class Player p where
  playCard :: [Card] -> p -> IO (Card,p)
  notifyFoo :: Event -> p -> IO p
  ...

このパターンはステート トランスフォーマーに非常に似ているように見えますが、どのように処理すればよいかわかりません。IO の上に追加のモナド変換子として書くとしたら、一日の終わりには 3 つの異なるモナドができました。この抽象化をうまく書くにはどうすればよいでしょうか。

明確にするために、通常の制御フローは次のようになります。
トリックをプレイするとき、最初のプレイヤーがカードをプレイし、次に 2 番目のプレイヤーがプレイし、最後に 3 番目のプレイヤーがカードをプレイします。これを行うには、ロジックplayCardでプレーヤーごとに関数 trice を実行する必要があります。その後、ロジックは、どのプレイヤーがトリックに勝つかを決定し、勝った情報をすべてのプレイヤーに送信します。

4

4 に答える 4

6

まず、型クラスの主な目的は、関数のオーバーロードを許可することです。つまり、単一の関数を異なる型で使用できるようにすることです。あなたは本当にそれを必要としないので、次の行に沿ったレコードタイプを使用する方が良いでしょう.

data Player = Player { playCard :: [Card] -> IO (Card, Player), ... }


第二に、IO を必要とするプレイヤーと必要としないプレイヤーの問題は、カスタム モナドで解決できます。私の運用パッケージの一部である TicTacToe のゲームに対応するサンプル コードを作成しました。

于 2011-04-24T13:11:08.913 に答える
4

はるかに優れた設計は、プレーヤーの種類の一部として IO を持たないことです。プレイヤーが IO を行う必要があるのはなぜですか? プレイヤーはおそらく情報を取得し、情報を送信する必要があります。それを反映したインターフェースを作る。IO が必要な場合は、playSkat によって実行されます。

これを行うと、IO を行わない他のバージョンの playSkat を使用できます。また、IO ではなくクラス メソッドを介してのみ対話するため、プレーヤーをより簡単にテストすることもできます。

于 2011-04-24T09:54:42.610 に答える
1

それが私が最終的に抽象化を設計した方法です:

エンジンがプレイヤーの 1 人に求めるものはすべて、 と呼ばれる大きな GADT にエンコードされてMessageいます。GADT のパラメーターは、要求された戻り値です。

data Message answer where
  ReceiveHand :: [Card] -> Message ()
  RequestBid  :: Message (Maybe Int)
  HoldsBid    :: Int -> Message Bool
  ...

playerMessageさまざまな種類のプレーヤーは、エンジンがメッセージをプレーヤーに送信して応答を要求できるようにする1 つの関数を持つ型クラスで抽象化されます。回答は でラップされているEitherため、回答を返すことができない場合 (たとえば、関数が実装されていない場合やネットワークがストライキ中など)、プレーヤーは適切なエラーを返すことができます。このパラメーターpは、プレーヤーがプライベート データと構成を保存するための状態レコードです。プレーヤーはモナドで抽象化され、m一部のプレーヤーは IO を使用でき、他のプレーヤーはそれを必要としません。

class Monad m => Player p m | p -> m where
  playerMessage :: Message answer -> p -> m (Either String answer,p)

編集

コンテキストを何度も入力することに満足できなかったため、別の Questionを尋ねたので、最終的にコードを変更して typeclass を具体化しましたPlayer。プレーヤーには状態がありませんが、部分的に適用された関数を使用してこれをシミュレートできます。詳細については、他の質問を参照してください。

于 2011-07-08T21:10:30.777 に答える
0

これについてはまったく考えていませんが、それでも検討する価値があるかもしれません。pここで、型クラス関数にin とout の両方があることに気付きました。pこれは、それらの "update" を意味すると推測しましたp。どういうわけか状態モナド。

class (MonadIO m, MonadState p m) => Player p where
  playCard :: [Card] -> m Card
  notifyFoo :: Event -> m ()

繰り返しますが、これは単なる自発的な考えです。私はそれが賢明である(またはコンパイル可能である)ことを保証しません。

于 2011-04-24T09:49:54.550 に答える