オセロプログラムの話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 直