モジュール化の練習に。
年末にゲームを作った。
http://amo12937.github.io/minmax_web/
年末時点では、2014年内に公開することを優先させたため、モジュールとしてはうまく分離できなかった(目指してはいたが…)
年始の4日で、テストとリファクタリングを行い、いくつかの機能をモジュールとして bower に公開したのでここにそれを記しておく。
クラス図
1回のゲームは、次の3つのオブジェクト間のメッセージパッシングによって進行する:
-
amo.module.game.game.Game
- ゲームの進行役
- ゲームの開始を受けて、適切なプレイヤーに
play
メッセージを送ることでゲームを進行させる(後述のシーケンス図参照)。
-
amo.module.game.player.Player
- プレイヤー
- 一つの戦略(
player.strategy
)に従って、1回のplay
を行う。
-
amo.minmax.module.board.Board
- ゲーム盤
- 盤面、スコア、現在のターン、選択可能な手の一覧など、ゲーム固有の要素を保持する。
ゲームのシーケンス図
各種モジュール
angular.amo.module.game.game
game.GameFsm
game の進行をステートマシンで表現している。game の状態はとてもシンプルで、初期状態 INIT
、プレイ中 PLAYING
、正常終了 DONE
、異常終了 STOPPED
の4種類しかない。
game.Game
GameFsm
自体を直接使うことはなく、Game
が内部で使用している。使う側は、Game
をインスタンス化し、start
メッセージを呼ぶことで状態を遷移させていく。
method | input | output | description |
---|---|---|---|
start | - | - | ゲームを開始する際に呼び出す。このメソッドは一度だけ呼べば良い。 |
pause | - | - | このメソッドが呼び出されると、次に resume が呼ばれるまでゲームを中断できる。ゲーム中断中も play は続行されるが、次の player へは処理は渡らず、delegate.notifyFinishedPlaying は呼ばれない。 |
resume | - | - | 中断中に呼び出されると、ゲームが再開される。play が終了していた場合は delegate.notifyFinishedPlaying が呼び出され、次の player へ処理が渡る。 |
paused | - | boolean | 中断中の場合に true を返す |
stop | - | - | ゲームを中止する。 |
game.Player
(これは、実際には JavaScript
のコードとしては現れない。JavaScript
にインターフェースを書く仕組みが無いため。)
game.Player
は、Game
から player
への要求インターフェースである。必要なメソッドは以下のとおり
method | required | input | output | description |
---|---|---|---|---|
play | o | - | finished | 1回の play を要求し、その play によってゲームが終了したかどうかを返す。output は boolean でも良いが、ユーザーの入力を待ったりするために、promise を返してもよい。 |
game.delegate
Game
は player
のことを知らないので、次に誰が play
するかを外部に問い合わせる。
method | required | input | output | description |
---|---|---|---|---|
getNextPlayer | o | - | game.Player | 次に play する player を返す |
他にも、ゲームの進行にあわせて notify
系のメッセージを投げるが、多いので省略。
angular.amo.module.game.player
https://github.com/amo12937/angular.amo.module.game.player
プレイヤー自体は何もせず、単に戦略 player.strategy
に従って、play
を行う。
player.strategy.Man
Man
は、人間の入力に従う戦略である。現在の盤面をもとにユーザーが手を入力し、それを持って 1度の play
とする。
player.strategy.Com.AlphaBeta
Com.AlphaBeta
は、アルファベータ法に基づくコンピュータの戦略である。アルファベータ法はミニマックスを改良したもので、ゲーム木の中の探索する必要のない枝を切って、効率化している。
コンピュータの手の選択がゲーム盤に依存していないのは、ゲーム盤が次のようにうまく抽象化されているからである。
player.Board
player.Board
インターフェースは、プレイヤーがゲーム盤に求める要求インターフェースで、次のメソッドが必要である:
method | description |
---|---|
current.turn() |
現在の手番を表す値を返す |
current.value(turn) |
手番を受け取り、現在の評価値を返す |
current.getSelectableList() |
現在選択可能な手の一覧を返す |
select(x) |
手を表す値を受け取り、その手を受理したかどうかを返す |
undo() |
盤面の状態を一つ前に戻す |
isFinished() |
ゲームが終了したかどうかを返す |
- 「手」
x
は、選択可能な一覧を順にselect
していくので、具体的には何なのかを問わない。それはObject
であっても、Array
であっても、数値や文字列であっても良い。 - 「手番」
turn
は単に current.value を得るために使われるため、これも具体的に何なのかを問わない。 -
current.value
は、数値を返す必要がある。(本当は順序が定義されたものであれば何でも良いがとしたかったが、内部で-Infinity
やInfinity
と比較している。)
amo.minmax.module.board
minmax
のゲーム盤 を表す。
このゲームにおいて、
-
turn
: 現在の手番- 先手、または後手が交互に返される
-
value
: ある手番の評価値- 自分のスコアから相手のスコアを引いた差である
- この値を大きくすることがゲームの目的である
-
selectableList
: 選択可能な手の一覧- 初手においては全てのパネル
- 以降の後手は縦1列
- 先手は横1列
- ただし、一度選ばれたパネルは省く
-
isFinished
: 終了条件- 選択可能な手が無くなった
となる。amo.minmax.module.board.Board
で実装されている。
おしまい:次の一手
仕事では時間やコストの折り合いもあってあまりモジュールに分けての開発というのをやれていなかったが、今回は完全に自分の趣味での開発だったので、やりたいように出来た。まだテストとリファクタリングとモジュール化が徹底できていないので、継続してやっていきたい。
- テスト
- リファクタリング
- モジュール化
- 横展開
- board を差し替えれば、「二人零和有限確定完全情報ゲーム」系を作ることができるので、オセロや、もう少し簡単な「Kalah」というのを作ってみたい。