オセロプログラムの話12 - setTimeoutの活用
setTimeの話
オセロプログラムの話11の続き。今回は、setTimeoutを上手に使って、処理の流れを綺麗にしてみます。以前、「JavaScriptのタイマー処理 setTimeoutとその活用」として、記事を書いたのですが、そこではsetTimeoutをタイマーとしてではなく、処理の流れを整えるために利用していました。setTimeoutは引数としてタイムアウトの時間と、そのときに実行される関数を指定します。例えば、3秒と指定した場合、一通り予定されている処理を終え、そこから3秒数えたあとで指定した関数が実行されるという話をしました。「一通り予定されている処理を終え」というのがここでのポイントです。
オセロプログラムの流れ
このプログラムの処理の流れは、「オブジェクト指向っぽくオセロを作る(まとめ)」でまとめています。概要だけ説明すると、プレイヤーにはコンピューターと人間の二種類います。ゲームを管理するOthelloオブジェクトはターンを交代する際、changeTurnというメソッドを使って、その内部でプレイヤーにターンが回ってきたことを通知します。このプレイヤーに通知を行うメソッドがPlayerクラスのturnメソッドです。このとき、コンピューターのプレイヤーは、自分が駒を置ける場所を探して、駒を置く処理、OthelloオブジェクトのdoFlipメソッドを実行します。これに対して、人間のプレイヤーはこのときは何もしません。人間のプレイヤーはこの後、オセロの盤のどこかのセルがクリックされたイベントをselectEventメソッドで受信して、駒を置く処理、doFlipを行うことになります。プレイヤー1が人間(p1)、プレイヤー2がコンピューター(p2)とした場合、以下のようなメソッド呼び出しの階層になります。
-Othello.changeTurn() -p1.turn() //人間なのでこのときは何もしない (イベント発生) -p1.selectEvent() -Othello.doFlip() -Othello.changeTurn() -p2.turn() -Othello.doFlip() -Othello.changeTurn() -p1.turn() (イベント発生(上と同じ)) -p1.selectEvent() -Othello.doFlip() -Othello.changeTurn() -p2.turn() -Othello.doFlip() -Othello.changeTurn() -p1.turn()
またコンピューター同士が対戦する場合、もっと面倒な呼び出し階層になります。以下、p1、p2ともにコンピューターです。
-Othello.changeTurn() -p1.turn() -Othello.doFlip() -Othello.changeTurn() -p2.turn() -Othello.doFlip() -Othello.changeTurn() -p1.turn() -Othello.doFlip() -Othello.changeTurn() -p2.turn() -Othello.doFlip() -Othello.changeTurn() …
これがゲームが終了するまで続きます。このような実装のばあい、コンピューターと人間の場合で、ここで登場した各関数の終了のタイミングが異なってしまいます。
そこでsetTimeoutを使います。
OthelloオブジェクトのchangeTurnメソッドの内部でプレイヤーのturnメソッドを呼び出す際に、あえてsetTimeoutの処理を利用します。
//OthelloオブジェクトのchangeTurnメソッド内部 //もともとturnには、現在のターンを担当するプレイヤーのオブジェクトが入っている (function(){ var _turn = turn; setTimeout(function() { _turn.turn(); //プレイヤーのturn()メソッドを呼ぶ }, 10); })();
このようにタイマーとしてturnメソッドを呼ぶことによって、turnメソッドがchangeTurnメソッドの内部で直接実行されることを防ぎます。ちなみにタイムアウトが発生したとき、turnメソッドの実行元はOthelloオブジェクトのchangeTurnメソッドではないので、クロージャのテクニックを使って値を保存しておく必要があります。これによって呼び出し階層は以下のように変わります。まず、p1が人間、p2がコンピューターの例。
-Othello.changeTurn() (タイマーイベント発生) -p1.turn() //人間なのでこのときは何もしない (クリックイベント発生) -p1.selectEvent() -Othello.doFlip() -Othello.changeTurn() (タイマーイベント発生) -p2.turn() -Othello.doFlip() -Othello.changeTurn() (タイマーイベント発生(以下繰り返し)) -p1.turn() (クリックイベント発生) -p1.selectEvent() -Othello.doFlip() -Othello.changeTurn()
次にp1、p2ともにコンピューターの場合の呼び出し階層
-Othello.changeTurn() (タイマーイベント発生) -p1.turn() -Othello.doFlip() -Othello.changeTurn() (タイマーイベント発生) -p2.turn() -Othello.doFlip() -Othello.changeTurn() (タイマーイベント発生(以下繰り返し)) -p1.turn() -Othello.doFlip() -Othello.changeTurn()
どちらの場合も、だいぶすっきりしました。
しかし、まだ少し問題があります。
以下の部分の呼び出し階層を見てください。
-p1.turn() -Othello.doFlip() -Othello.changeTurn()
p1.turnの中でOthello.doFlipが呼ばれ、そしてその中でOthello.changeTurnが実行されます。しかしchangeTurnの実行が終わったあと、再びp1.turnに処理が戻ってきます。changeTurnが実行された時点で相手のターンにするつもりなのに、p1.turnに処理が戻ってきているわけです。これは良く考えると気持ち悪いですよね。そこで、Othello.doFlipがOthello.changeTurnを呼び出す際にも、setTimeoutを利用して間接的に呼び出すようにします。
//doFlip内の処理 (function(){ var _changeTurn = changeTurn; setTimeout(function() { _changeTurn(); //ひっくり返せたのでターン交代 }, 10); })();
これによって呼び出し階層は以下のように変わります。p1が人間、p2がコンピューターの例。
-Othello.changeTurn() (タイマーイベント発生) -p1.turn() //人間なのでこのときは何もしない (クリックイベント発生) -p1.selectEvent() -Othello.doFlip() (タイマーイベント発生) -Othello.changeTurn() (タイマーイベント発生) -p2.turn() -Othello.doFlip() (タイマーイベント発生) -Othello.changeTurn() (タイマーイベント発生(以下繰り返し)) -p1.turn() (クリックイベント発生) -p1.selectEvent() -Othello.doFlip() (タイマーイベント発生) -Othello.changeTurn()
次にp1、p2ともにコンピューターの場合の呼び出し階層
-Othello.changeTurn() (タイマーイベント発生) -p1.turn() -Othello.doFlip() (タイマーイベント発生) -Othello.changeTurn() (タイマーイベント発生) -p2.turn() -Othello.doFlip() (タイマーイベント発生) -Othello.changeTurn() (タイマーイベント発生(以下繰り返し)) -p1.turn() -Othello.doFlip() (タイマーイベント発生) -Othello.changeTurn()
setTimeoutのおかげでかなりすっきりしちゃいました。嬉しいです。
setTimeoutの本来の使い方
setTimeoutを本来の役割であるタイマーとして使ってみます。今回のオセロプログラムでは、人間とコンピューターが対戦した場合、人間が駒を置くと、コンピューターのターンになるわけですが、コンピューターは自分のターンが回ってくるとすぐに駒を置くという問題がありました。これはどういうことかというと、人間のプレイヤーが自分が駒を置いて、相手の駒をひっくり返したのをちゃんと確認する間もなく、コンピューターが駒を置いてしまうということを意味しています。そこでコンピューターの処理を少し遅らせます。今回コンピューターを表すComputerオブジェクトに手を加えました。Computerオブジェクトの変更部分だけ書いておきます。
var Computer = function(piece, name){ … this.timer = false; … /** * ターンが回ってくると実行(外部公開) */ this.turn = function(){ if(this.timer == false){ var self = this; (function(){ var _self = self; setTimeout(function() { _self.timer = true; _self.turn(); //プレイヤーのturn()メソッドを呼ぶ _self.timer = false; }, 800); })(); return; } … (doFlipで駒を置く処理) }
こちらも、以前「JavaScriptのタイマー処理 setTimeoutとその活用」として書いた記事の中で説明していたテクニックです。このturnメソッドは最初にタイマーをセットしてreturnして関数を抜け出します。そして、一定時間たった後で、自分自身を再び呼ぶようにしています。再び呼ばれた際には、this.timerの値はtrueになりますので、タイマーがセットされずに、turnメソッドの後半部分へと進むことができます。これを使うことによって、コンピューターがワンテンポ置いて、駒を置いてくれるようになります。
今回は以上でおわりです。最後に実行可能なソースを添付しておきます。ちなみにコードが長くなってきたので、JavaScriptのファイルをいくつかに分けました。
oop-othello12.zip