Haskellでポーカーを作ろう〜第七回 プロジェクトを整理するよっ〜
外はすっかり真夏日よりですが、今日も冷房の前ででプログラミング充しています。
嘘です、ずっとスプラトゥーンやってました。
結果として相変わらずGithubのContributionsは真っ白です。
どもども、ちゅーんさんです。
このエントリは、ちゅーんさんによるポーカー開発連載記事の第七回目です。
過去のエントリはこちら
第一回 リストのシャッフルとカードの定義
第二回 ポーカー・ハンドの判定をする 前編
第三回 ポーカー・ハンドの判定をする 中編
第四回 ポーカー・ハンドの判定をする 後編
第五回 カードの入れ替え処理を作る
第六回 CPU対戦機能を付けよう
前回までは、やや場当たり的に作って行きましたが、 そろそろ程度整理しないと全体像が掴みにくい規模になってきたようです。
そこで今回は、Haskellのビルドツールについて簡単に紹介/説明し、 ここまで書いたプログラムをプロジェクトとして管理できるように移行します。
その後、前回までに作成した関数を洗い出して、モジュール構成を整理しましょう。
前準備とか
まず、前回までの内容を手順通りに勧めていれば、以下の3ファイルが出来ているはずです。
- Main.hs
- Cards.hs
- Hands.hs
Main.hs
をPoker.hs
に書き換えて、main
関数の名前をsimpleGame
関数に変更してください。
そして、Poker.hs
のモジュール宣言部を次のように書き換えます。
module Game.Poker ( module Game.Poker.Hands , module Game.Poker.Cards , simpleGame ) where ...
その上で、新しくMain.hs
を作りなおしましょう。
module Main where import Game.Poker main :: IO () main = simpleGame
これらのファイルを以下のように配置します。
. ├── app │ └── Main.hs └── src └── Game ├── Poker │ ├── Cards.hs │ └── Hands.hs └── Poker.hs
srcディレクトリ以下に基本的なプログラムは書くようにし、 appディレクトリは、アプリケーション本体のソースコードを配置する事にしましょう。
こうする事によって、例えば「GUI版を作りたい」といった時に、このプロジェクトをインストールし、
Game.Poker
モジュールを読み込む事によって、ポーカーゲームを実装するための全ての関数を再利用する事ができます。
開発者はUIだけ作れば良くなるというわけですね。
ディレクトリの構成に合わせて、各モジュールのモジュール名を変えます。
- Cards.hs ->
Game.Poker.Cards
- Hands.hs ->
Game.Poker.Hands
併せて、各ファイルのimport
文も書き換えましょう。
ビルドのための設定を行う
これまで紹介したcabalの使い方といえば、cabalコマンドを使ってパッケージをインストールするだけでしたが、 cabalにはプロジェクトをビルドして、パッケージや実行ファイルをインストールしたりするビルドツールとしての機能もあります。
cabal自体はかなり優秀なツールなのですが、依存関係が壊れやすいという少々ややこしい問題を抱えており、 Haskellで巨大なライブラリを使ったり、色々なライブラリを一緒に使おうとした時に問題が発生する事が多く、 長らく問題になっていまして、それがほんの数カ月ほど前にリリースされたstackというビルドツールによって解決される事になりそうな状況なのです。
基本的にstackの方を推奨したいのですが、 stackはまだ開発中のツールですし、cabalの上で動作するものでもあるので、今回は両方の使い方を説明しようと思います。
いちおう、どちらかが使えれば良いという方に向けて、それぞれ独立して読んでも使えるように説明します。
- ちゃんと理解したい人 -> 「cabalの使い方」「stackの使い方」両方読む
- 使えれば良い人 -> stackの使い方を読む
- 枯れていない技術は信用できない人 -> cabalの使い方を読む
cabalの使い方
というわけで、ビルドツールとしてcabalを使う方法を簡単にご説明しましょう。
cabalを使ったパッケージ管理を行うためには、*.cabal
という拡張子の設定ファイルを作成する必要があります。
cabal init
というコマンドを実行する事によって、対話形式によって入力された内容や現在のディレクトリの状態等といった
情報を元に、ある程度の項目が埋められた状態のcabalファイルを簡単に生成する事ができます。
$ cabal init Package name? [default: draw-poker] Package version? [default: 0.1.0.0] Please choose a license: * 1) (none) 2) GPL-2 3) GPL-3 4) LGPL-2.1 5) LGPL-3 6) AGPL-3 7) BSD2 8) BSD3 9) MIT 10) ISC 11) MPL-2.0 12) Apache-2.0 13) PublicDomain 14) AllRightsReserved 15) Other (specify) Your choice? [default: (none)] 8 ...
残念ながら、このコマンドによってすぐにビルド出来る状態にはなりません。 いくつか追加設定が必要な項目があるのですが、今回は以下のようにcabalファイルを設定します。
-- Initial draw-poker.cabal generated by cabal init. For further -- documentation, see http://haskell.org/cabal/users-guide/ name: draw-poker version: 0.1.0.0 synopsis: playing draw poker description: for blog entry homepage: http://tune.hateblo.jp/entry/2015/05/12/023112 license: BSD3 license-file: LICENSE author: Tokiwo Ousaka maintainer: its.out.of.tune@gmail.com -- copyright: category: Game build-type: Simple -- extra-source-files: cabal-version: >=1.10 library hs-source-dirs: src exposed-modules: Game.Poker , Game.Poker.Hands , Game.Poker.Cards build-depends: base >= 4.6 && < 4.7 , random-shuffle >=0.0&& <0.1 , safe >=0.3 && <0.4 default-language: Haskell2010 executable draw-poker main-is: Main.hs -- other-modules: -- other-extensions: build-depends: base >=4.6 && <4.7, draw-poker hs-source-dirs: app default-language: Haskell2010 source-repository head type: git location: https://github.com/tokiwoousaka/draw-poker
変更箇所については以下のdiffをご覧ください。
https://gist.github.com/tokiwoousaka/37c93cd7a067dbac1483/revisions
各項目について詳しい説明は行いません、 得に設定する事の多い、以下の二点のみ、認識しておいてください。
- モジュールが増えた場合は
exposed-modules
に項目の追加が必要 - 依存するパッケージが増えた場合、
build-depends
に項目の追加が必要
それ以外で今後修正が必要な場合は都度説明します。
ポイントとしては、
description
の項目を設定したlibrary
およびexecutable
両方の項目executable
のソースファイルがapp/Main.hs
を指定しているようになっている必要がある- 外部ライブラリへの依存しているのは
executable
ではなくlibrary
executable
はdraw-poker
パッケージそのものに依存している旨記載が必要source-repository
の設定は任意(しなくても良い)
といったところでしょうか。
その他、以下の基本的な機能についてはざっくりと覚えておきましょう。
- 動作確認
cabal repl
コマンド:library
をGHCiで読み込みますcabal run app/Main.hs
コマンド:app/Main.hs
を実行します
- インストール
cabal install
コマンド:ライブラリおよび実行ファイルのインストール- 初回はその前に
cabal configure
を実行する必要があります - cabalファイルの
library
で設定されている情報を元にライブラリをインストール - cabalファイルの
executable
で設定されている
- 初回はその前に
- ドキュメント作成
cabal haddock
コマンド:dis/dock/html
配下にドキュメントを作成します- ビルド通ったら一度は確認しときましょう。とても便利です。
なお、全体的な説明については、以下の記事を参考にすると良いでしょう。
http://itpro.nikkeibp.co.jp/article/COLUMN/20121106/435201/?ST=develop&P=2
stackの使い方
この節では、stackの使い方を説明します。
stackのインストールは、以下のREADMEの、How to Install を参照してください。 (今の所、全環境へのインストール方法の日本語訳はされていないのです・・・。)
https://github.com/commercialhaskell/stack
stack new
コマンドを利用する事により、新規プロジェクトを作成する事ができます。
既存のソースがある場所では実行する事ができませんので、
空のフォルダでstack new
コマンドを利用し、プロジェクトを作成したら、
予め作っておいたソースコードをコピーして来てください。
生成されたファイルと、コピーしたファイルを含めて、以下のようなディレクトリ構成になっていればOKです。
. ├── app │ └── Main.hs ├── LICENSE ├── new-template.cabal ├── Setup.hs ├── src │ └── Game │ ├── Poker │ │ ├── Cards.hs │ │ └── Hands.hs │ └── Poker.hs ├── stack.yaml └── test └── Spec.hs
そして、cabalファイルをdraw-poker.cabal
にリネームし、以下を参考にして、内容を書き換えます。
name: draw-poker version: 0.1.0.0 synopsis: playing draw poker description: for blog entry homepage: http://tune.hateblo.jp/entry/2015/05/12/023112 license: BSD3 license-file: LICENSE author: Tokiwo Ousaka maintainer: its.out.of.tune.this.my.music@gmail.com category: Game build-type: Simple cabal-version: >=1.10 library hs-source-dirs: src exposed-modules: Game.Poker , Game.Poker.Hands , Game.Poker.Cards build-depends: base >= 4.7 && < 5 , random-shuffle , safe default-language: Haskell2010 executable draw-poker hs-source-dirs: app main-is: Main.hs ghc-options: -threaded -rtsopts -with-rtsopts=-N build-depends: base , draw-poker default-language: Haskell2010 test-suite draw-poker-test type: exitcode-stdio-1.0 hs-source-dirs: test main-is: Spec.hs build-depends: base , draw-poker ghc-options: -threaded -rtsopts -with-rtsopts=-N default-language: Haskell2010 source-repository head type: git location: https://github.com/tokiwoousaka/draw-poker
変更すべき箇所については以下のdiffを参照していただくと良いでしょう。
https://gist.github.com/tokiwoousaka/802654fd6e4ab3ed2f0c/revisions
http://tune.hateblo.jp/entry/2015/07/13/034148
各項目について詳しい説明は行いません、 得に設定する事の多い、以下の二点のみ、認識しておいてください。
- モジュールが増えた場合は
exposed-modules
に項目の追加が必要 - 依存するパッケージが増えた場合、
build-depends
に項目の追加が必要
それ以外で今後修正が必要な場合は都度説明します。
ポイントとしては、
- パッケージ名を
new-template
から変更 synopsis
やdescription
等のメタ情報を設定したsource-repository
の設定は任意(しなくても良い)
といった所でしょうか。 その他、以下の基本的な機能については覚えておきましょう。
- 動作確認
stack repl
コマンド:library
をGHCiで読み込みますstack runghc app/Main.hs
コマンド:app/Main.hs
を実行します
- インストール
stack install
コマンド:ライブラリおよび実行ファイルのインストール- cabalファイルの
library
で設定されている情報を元にライブラリをインストール - cabalファイルの
executable
で設定されている
- cabalファイルの
- ドキュメント作成
stack haddock
コマンド:- ビルド通ったら一度は確認しときましょう。とても便利です。
なお、残念ながら現状、日本語の資料は整いきっていない状況です。 以下の記事を参考にすると良いでしょう。
http://qiita.com/tanakh/items/6866d0f570d0547df026
Hackageへのアップロード
あまり詳細には説明しませんが、開発したアプリケーションやライブラリをHackageにアップロードするための機能として、
cabalにはcabal upload
、stackにはstack upload
というコマンドが存在します。
本連載で開発中のポーカーも、Hackageにアップロード済です。
http://hackage.haskell.org/package/draw-poker
プロジェクトの整理
さて、これでひと通りビルドツールの説明を終えました、動く事が確認出来たはずです。
いよいよ開発っぽくなってきましたね。
ところで、前々回/前回で作成した関数はMain.hs
(Poker.hs
に変更したのでした)に纏めて作ったのでした。
このままでは今後整理し切れなくなる可能性があるため、これらをちゃんと整理し、機能毎のモジュール化を行いましょう。
モジュール化
まず、現状のMain.hs
モジュールに宣言されている型や関数を列挙してみましょう。
前々回では「ハンドの入れ替え処理」を作りました。そして前回作成したのAIの思考ルーチンもありますね。
- ハンドの入れ替え
- 型
type DiscardList = [Card] -- 捨て札
- 関数
getHand :: Deck -> Maybe (Hand, Deck)
drawHand :: Deck -> DiscardList -> Hand -> Maybe (Hand, Deck)
getDiscardList :: Hand -> IO (Maybe DiscardList)
toIntList :: String -> Maybe [Int]
selectByIndexes :: [a] -> [Int] -> Maybe [a]
- 型
- AIの思考ルーチン(カードの入れ替え)
- 関数
aiSelectDiscards :: Hand -> DiscardList
nOfKindDiscards :: Hand -> DiscardList
- 関数
- 勝敗判定
- 関数
judgeVictory :: (PokerHand, Card) -> (PokerHand, Card) -> Ordering
- 関数
- プロトタイプ
- 型
data Player = Player | Enemy deriving Eq
- 関数
simpleGame :: IO ()
showPlayerName :: Player -> String
matchPoker :: (Hand, Deck) -> IO ()
playPoker :: Hand -> Deck -> Player -> IO ((PokerHand, Card), Deck, Hand)
inputDisuse :: Hand -> IO DiscardList
aiDisuse :: Hand -> IO DiscardList
printResult :: Hand -> Hand -> (PokerHand, Card) -> (PokerHand, Card) -> IO ()
printHand :: DiscardList -> Hand -> Player -> IO ()
printHand dis hand player =
ynQuestion :: String -> IO a -> IO a -> IO a
showChangeHand :: DiscardList -> Hand -> String
- 型
src/Game/Poker
ディレクトリ以下に新たにAI.hs
およびHands.hs
を用意します。
で、この4つに分類した関数を、以下のとおりモジュール分けしましょう。
- ハンドの入れ替え:
Hands.hs
- AIの思考ルーチン:
AI.hs
- 勝敗判定:
Hands.hs
- プロトタイプ:Simple.hs
結果として、ソースコードは以下のような配置になっているはずです。
. ├── app │ └── Main.hs └── src └── Game ├── Poker │ ├── AI.hs <- 追加 │ ├── Cards.hs │ ├── Hands.hs │ └── Simple.hs <- 追加 └── Poker.hs
移動させた各関数をエクスポートした上で、Poker.hsを次のように書き換えます。
module Game.Poker ( module Game.Poker.Hands , module Game.Poker.Cards , module Game.Poker.AI ) where import Game.Poker.Hands import Game.Poker.Cards import Game.Poker.AI
このように、module モジュール名
のようにエクスポート文を記述する事によって、
そのモジュールがエクスポートしている全ての関数をエクスポートする事ができ、
Poker.hs
はポーカー開発に便利なあらゆるモジュールを全て一気にエクスポートする事ができるようになります。
また、ポーカーゲーム本体になるIO処理は、このモジュールに作っていく事にしましょう。
プロトタイプの扱い
さらに、Game.Poker
モジュールではエクスポートしなかった、Simple.hs
について考えます。
ここでエクスポートしているsimpleGame
関数は、前回までで作成したプロトタイプですね。
このプロトタイプも遊べるものにはなっているので、捨ててしまうのももったいないですし、 得にリソースを消費するほどのものでも無いですから、一緒に配布する事を考えましょう。
appフォルダ以下にSimple.hs
を追加し、cabalファイルに以下のような記述を追加します。
executable draw-poker-simple hs-source-dirs: app main-is: Simple.hs ghc-options: -threaded -rtsopts -with-rtsopts=-N build-depends: base , draw-poker default-language: Haskell2010
そして、追加したSimple.hs
で、Poker.Simple
モジュールを読み込み、main関数でsimpleGame
関数を呼び出すようにします。
同時に、先ほどまでsimpleGame
を呼んでいたMain.hs
のmain
関数については、ひとまずHello Worldでも書いておけば良いと思います。
これらをひと通り設定したら、ビルドが実行してみて、
上手く行ったらcabal install
またはstack install
を実行し、
2つのアプリケーションがインストールされて実行出来るか確認すると良いでしょう。
色々と設定する事が多くて大変かもしれません、stack向けに構成したプロジェクトをGithubに上げてあるので、 もし上手く行かない場合はこちらも参考にしてみてください。
https://github.com/tokiwoousaka/draw-poker/tree/aa33fc7fdbf3e1292cbc33a580bd21758ae4a6b6
まとめ
というわけで、今回はビルドツールの使い方を説明し、モジュールの整理を行いました。
一定以上大きなプログラムを開発する上ではプログラミング言語そのものだけではなく、 さまざまな開発ツールの使い方を習得する必要がある事は、言うまでもないでしょう。
これらツールの使い方は本記事では解説し切れない事も多いので、 cabalやstack等ツールの使い方については、各々でも色々と調べてみてください。
さて、せっかくプロジェクトを整えたのですから、
この構成を利用して、プログラムの検証を行えるようになると良いですね。
というわけで、次回はHUnitやQuickCheckといったテスト用のライブラリについて説明し、
テストコードの書き方について説明して行きます。
それではまた、ノシノシ