Haskellに入門して1年位経ったのでライフゲームを作ってみた話

この記事はHaskell (その3) Advent Calendar 2017の18日目の記事です。

qiita.com

最近忙しくてHaskellに触れる時間がなかったため陳腐な内容の記事になってしまったことご了承ください。

前置き

Haskellに入門してから年(半)位経ったとしています。

実はもっと前からHaskellを少し触りだけはやっていたのですが、本格的に本を買ったりなどしてまともに勉強しはじめたのは今年からです。

そこでゲームを作るということを目標に学んでいたわけですが、さすがに1年ちょっとじゃ豪華なGUIを作るのは難しいのでTUIでライフゲームを実装することを目標としました。

使ったライブラリはHSCursesというものでncursesのHaskell版です。

NCursesとかいうのもあったのですが、自分の環境でうまく動いたのはHSCursesだったのでこれを採用。

状態系モナドなどはあまり使い方がわからなかったので全て関数の引数で状態を引き継ぐ形で実装しました。

これは今後いつか状態系モナドで置き換えていこうと思います。

ソースコードを見てみよう

ライフゲームソースコード上でHSCursesとかをどこで使ったのかを見ていきます。

フィールドの表示 - Curses.wAddStr

cursesOutput :: Field -> Int -> IO ()
cursesOutput field lineCount = do
    if lineCount /= 0 then do
        Curses.wAddStr Curses.stdScr (lineToString (field !! ((length field) - lineCount)))
        Curses.addLn
        cursesOutput field (lineCount - 1)
    else
        return ()

wAddStrは以下のような型シグネチャになっています。

wAddStr :: Window -> [Char] -> IO ()

WindowにString型に変換したライフゲームのフィールドを描画するための関数です。 これを再帰的に行うことで複数行に渡るライフゲームのフィールドを描画しています。

で、これをメインループ内では以下のように使っています。

Curses.timeout 1000
let sentineledField = addSentinel $ addSentinel field
CursesHelper.gotoTop
cursesOutput sentineledField (length sentineledField)
Curses.move y x
Curses.refresh

2マス分番兵を置くための関数を適用したフィールドをgotoTop関数を使って一番上からループ毎に描画しています。

ゲームへの入力操作 - Curses.getch

c <- CursesHelper.displayKey <$> Curses.decodeKey <$> Curses.getch

case c of
    "q" -> quitTask
    "f" -> loop field (changeMode mode) (x,y)
    "h" -> loop field mode ((max (x-1) 0),y)
    "l" -> loop field mode ((min (x+1) (width-1)),y)
    "k" -> loop field mode (x,(max (y-1) 0))
    "j" -> loop field mode (x,(min (y+1) (height-1)))
    "o" -> loop (fieldFlip field (x+2,y+2)) mode (x,y)
     _   -> nextLoop mode

cに入力した文字をString型に変換した値を突っ込んで、caseでそれぞれの値による操作をしています。

こんな感じでHSCursesを使って実装していきました。

まとめ

Haskellでゲームを作るというチャレンジはとてもおもしろいものでした。

次はSDLなどを使ってもっとクオリティの上がったゲームを作りたいなと思っています。