読者です 読者をやめる 読者になる 読者になる

Haskellでハトクラを表現する

この記事はHaskell Advent Calendar 17日目の記事です。

What is ハトクラ

ハトクラとは、ドミニオンの変種みたいなカードゲームです。
ただし、カードの連結が続く限りカードをプレイできるというルールが加わっています。

f:id:haru2036:20141202231210p:plain

こんな感じ
……
ん……?連結……?

これってモナドになりませんか?(これってトリビアになりませんか?的ノリ)

そこで、今回はハトクラの一部(アクションフェーズ, またはメインフェーズ)を表現するモナドを作ってみようと思います。

やってみる

これはおそらくStateモナドの変種みたいな感じで行けるのではと思いました。ただ、一番後ろのカードに連結マークがない(=もう連結できない)状態をどうにかして表現しないといけません。
まずはなんかと合成できないかと考えてみたりしてたのですが、何と合成しようかなあと考えてました。


EitherとStateならLeftでも値が持てるけどそのあとの計算がされなくなるのでピッタリですね。ありがとうございます! 一般的には失敗した時の情報とかを持たせるEitherですが、そんな使い方もあるとは。
というわけで、アクションフェーズの型を考えてみました。

data Field = Field {
                    inheritanceRights :: Int,
                    hand :: Cards,
                    deck :: Cards,
                    trash :: Cards
                   }

type Cards = [Card]

type Card = Coins -> ActionPhase

type Coins = Int

type ActionPhase = StateT Field (Either (Coins, Field)) Coins

StateTとEitherを合成しました。Fieldはゲーム内の自分に関する大域的な状態で、Coinsは利用できるコインの枚数です。

ただ、なんかこれだと定義がごちゃごちゃしてる気がしてちょっとイケてないなと思ったので、StateとEitherTを使ったバージョンも書いてみたのですが、あんまりどっちもイケてなかったので使いやすそうなこっちのバージョンを採用しました。

とか全然間違ってた事を考えてたのですが、ツッコミをもらえて助かりました。

理解が遅くて申し訳ない、ありがとうございます。

それも含めた版がこちら:

type ActionPhase = EitherT Coins (State Field) Coins

一部のカードを実装してみる

さて、幾つか実際のゲームに登場するカードを実装して試してみましょう。

まずはこれがなきゃ始まらない農園とか都市とか。

farm :: Card
farm c = do 
  return (c + 1)

順当 and 単純。当たり前やがなッて感じです。

あとは、コレ単体では存在しませんがよく出てくる次にカードが繋げない終端のカードです。

stop :: Card
stop c = do
  fld <- (lift get)
  left c

EitherTの機能を使ってその時点での状態をLeftにくるんで返すだけです。 単純です。

他には個人的に好きなカード、錬金術士とかもよいですね。

alchemist :: Card
alchemist c = do
  fld <- lift get
  lift $ put $ drawCards 2 fld
  return c
addHands :: Field -> Cards -> Field 
addHands f cds = setHands f ((f ^. hand) ++ cds)

setHands :: Field -> Cards -> Field
setHands f cds = f & hand .~ cds

drawCard :: Field -> Field
drawCard f = let card = head (f^.deck)
                 deckTail = tail (f^.deck)
                 newHand = card : (f^.hand) 
             in (f&hand .~ newHand)&deck .~ deckTail

drawCards :: Int -> Field -> Field 
drawCards count f
  | count > 0 = drawCard $ drawCards (count - 1) f
  | count == 0 = f
  | count < 0 = error "count should be > 0"

2枚山札から手札に引く事ができるカードです。 これまでFieldを触る処理がなかったので全然書きませんでしたが、Lensは更新を簡単に出来てよいですね、なお、今回の処理に使うには本家はでかすぎるのでちゅーんさんのLensを使わせていただきました。

Haskellまだまだ修行の身な上時間が少なくやっつけ気味になってしまいましたが、とりあえずこんなかんじでハトクラをモナドにできそう、という感じはもてました。

ただし、二股になるのとかは考慮していないので完全にはまだまだ程遠いですね…… ちなみに、githubリポジトリあります。需要全くなさそうだし適当ですが。

haru2036/heart-of-crown-monad · GitHub

余談: Stateモナドの仕組みを理解してから、それを使うためには仕組みまで理解することがないのを知りました。 普通に楽しかったし興味深い仕組みにより実現されてることを知れたのでよいですが、Haskellって全体的にただ使うのはらくらくだけど理解するのは大変ですね……