LoginSignup
14
7

More than 5 years have passed since last update.

Haskellで作るコンソールLife Game

Last updated at Posted at 2018-06-21

Fortranと呼ばれる古代言語を書き連ねる日々に嫌気が差したので、副作用という穢れを払うためHaskellの力を借りることにしました.
いまもHaskellの勉強中なのですが、結局モナドは、関数や値を包み込みこんで<<=といった演算子や特定の関数で操作を行うことを明示する、OOPでいうカプセル化の概念に近いものなのかな?
まぁやっぱりプログラミングを学ぶには見るより、書くのが一番だと思うので、コンソール上で動作する簡単なLifeGameを書いてみました.

ゲーム状態の定義

フィールドの状態としてはセルをその座標Pos型で示し、全体としてはBoard型で管理します.
Boardは生きたセルを要素とするPos型のリストです. また必要なライブラリもimportしておきます.

import Control.Monad
import System.Random

type Pos = (Int, Int)
type Board = [Pos]

フィールドのサイズ

変更しやすくするため横幅widthと縦幅heightも関数で定義しておきます.

width :: Int
width = 50

height :: Int
height = 50

各セルの状態チェック

次にPos型の座標からBoard内にそのセルがあるかをチェックし、そのセルが生きているかをBool型で返す関数isAliveを用意します. また逆にisEmpty関数はセルにライフがいないかを返します.

isAlive :: Board -> Pos -> Bool
isAlive b p = p `elem` b

isEmpty :: Board -> Pos -> Bool
isEmpty b p = not (isAlive b p)

周りのセル取得

neighbers関数はセル座標Posからその周り8方向のセルを取得します. 今回はフィールドから外に出ると逆側に出る周期条件を採用しているため、wrap関数で境界部分の周囲のセル取得に対応します.

wrap :: Pos -> Pos
wrap (x, y) = (((x-1) `mod` width) + 1,
               ((y-1) `mod` height)+ 1)

neighbers :: Pos -> [Pos]
neighbers (x, y) = map wrap [(x-1, y-1), (x, y-1),
                             (x+1, y-1), (x-1, y),
                             (x+1, y),   (x-1, y+1),
                             (x, y+1),   (x+1, y+1)]

次世代のライフ

liveneighbers関数は周囲の生きたセルの数を「Pos型の引数から返す関数」を返す関数です.
そしてsurvivorsは現世代から次世代で生き残るセルのリストを返します.
一方birthsは次世代で新たにライフが生まれるセルのリストを返します.
births内で使っているrmdupsはリストから重複要素を除いて1つにします.
そして最後にsurvivorsbirthsからnextgenで次世代のライフを取得します.

liveneighbers :: Board -> Pos -> Int
liveneighbers b = length . filter (isAlive b) . neighbers

survivors :: Board -> [Pos]
survivors b = [p | p <- b, liveneighbers b p `elem` [2, 3]]

rmdups :: Eq a => [a] -> [a]
rmdups [] = []
rmdups (x:xs) = x : rmdups (filter (/= x) xs)

births :: Board -> [Pos]
births b = [p | p <- rmdups (concatMap neighbers b),
                isEmpty b p,
                liveneighbers b p == 3]

nextgen :: Board -> Board
nextgen b = survivors b ++ births b

描画用関数

次にコンソールに表示するための関数を用意します.
clsはコンソールの画面をクリアします. そしてgotoでカーソルの場所を移動させ、writeatでライフを表示します. 実際に画面表示をするのはshowcellsで、アクションリストの要素アクションを順次実行するseqn関数を使って描画していきます.

cls :: IO ()
cls = putStr "\ESC[2J"

goto :: Pos -> IO ()
goto (x, y) = putStr ("\ESC[" ++ show y ++ ";" ++ show x ++ "H")

writeat :: Pos -> String -> IO ()
writeat p xs = do goto p
                  putStr xs

seqn :: [IO a] -> IO ()
seqn [] = return ()
seqn (a:as) = do a
                 seqn as

showcells :: Board -> IO ()
showcells b = seqn [writeat p "O" | p <- b]

ゲームサイクル

最後にゲーム本体を定義します.
waitは世代間で描画の間を取るためだけの関数です.

lifegame :: Board -> IO ()
lifegame b = do cls
                showcells b
                wait 5000
                lifegame (nextgen b)

初期状態

lifegame関数を呼ぶには初期状態のBoardを与えてあげないと行けないので、それを返す関数を用意します.
makeLifeで乱数を使って任意の場所のセル座標を返します.
そしてinitBoardで指定した数のライフを持つBoardを取得します.今回はBoardサイズの1/5の数としています.

makeLife :: IO Pos
makeLife = do
        x <- getStdRandom $ randomR(1, width) :: IO Int
        y <- getStdRandom $ randomR(1, height) :: IO Int
        return (x, y)

initBoard :: IO Board
initBoard = replicateM (width * height `div` 5) makeLife

完成!!!

main :: IO ()
main = lifegame =<< initBoard

さぁ実行してみましょう!!!!!

lifegame

...........
はい!!!すばらしいですね!!!すばらしいです!!!

..........
閑話休題

関数型でまともにコード書いたのは初めてですが、haskellの型->型の型を強く意識させる仕様は関数の細分化を促し、そのため関数それぞれの独立性が強いように感じました. 安定性・安全性が高いといわれているのもなんだかわかる気がします. なにより単純な関数の組み合わせで複雑な処理が簡潔に書けるのは気持ちいいですね. 次は練習がてら前々からやろうと思ってたProject EulerをHaskellで書いてみようかな.

14
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
7