オブジェクト指向っぽくオセロを作る7

続きです。今回はPlayerクラスについて考えて見ます。先に前回作ったPlayerクラスを再掲載しておきます。

/**
 * プレイヤーを表すクラス
 */
var Player = function(piece, name) {
	/**
	 * 名前を返す
	 */
	this.toString = function() {
		return name;
	};
	/**
	 * このプレイヤーの駒を返す
	 */
	this.getPiece = function() {
		return piece;
	};
	/**
	 * ターンが回ってくると実行
	 */
	this.turn = function() {
	};
};

プレイヤーの動作についてもう少し深く考えてみます。

オセロは通常、人と人が対戦するものですが、今回のプログラムでは人とコンピューター、またはコンピューターとコンピューターが対戦することも想定しています。つまりプレイヤーには人間とコンピューターの二種類いるわけです。ここで一つポイントがあります。それは、Othelloオブジェクトから見た場合、人間もコンピューターも同じプレイヤーだという点です。人間だから、コンピューターだからといって、Othelloオブジェクトの側で特別な処理をしてはいけません。Othelloオブジェクトから見ると、人間もコンピューターも同じように扱えるように実装します。

プレイヤーがもしコンピューターだった場合、どういう処理が必要になるのか考えてみましょう。まず最初にプレイヤー側のturnメソッドが呼ばれます。すると、コンピューターはどこに駒を置くのか考えます。ここでは単純に全てのセルをcheckPieceでチェックして、駒が置ける場所があれば、そこに駒を置く、つまりdoFlipするという処理を実装します。

逆にプレイヤーが人間だった場合はどうでしょう。人間の場合、turnメソッドが呼ばれたからといって、すぐに駒を置くことはできません。先ほども書きましたが、turnメソッドは単純に番が回ってきたということを伝えているだけです。つまりturnメソッドの中身は空っぽでもかまいません。重要なのは、最終的にdoFlipメソッドを呼ぶかどうかということなのです。今回、人間は描写されたオセロの盤をクリックすることで、doFlipメソッドを呼ぼうと思います。今まで、このプログラムは連続して手続き的に実行されていましたが、ここではオセロの盤がクリックされるというイベントを待っている状態になります。つまりイベント駆動型プログラムになります。

人間から作っていく

今回コンピューターは後回しにして、先に人間だけ作ってしまいます。人間のプレイヤーを作ろうと考えると、オセロの盤がクリックされたときに、それを処理するためのメソッドが必要になります。オセロの盤の表示にかかわっているのは、Viewオブジェクトです。今回、Viewオブジェクトで作成するセルのDOM要素に対して、クリックされたときイベントが発生するようにします。そしてこのイベントからOthelloオブジェクトのメソッドが呼ばれ、現在のターンを担当しているプレイヤーへと伝えられます。つまり、Viewオブジェクト、Othelloオブジェクト、Playerクラスの三つに変更を加える必要があります。

まずViewオブジェクトについてですが、各セルのDOM要素を作るときにmakeElementというメソッドを利用していました。ここで各セルのDOM要素に対して、onclickというイベントを埋め込みます。ここで作られたオセロの盤の各セルがクリックされた場合、Othelloオブジェクトにその主旨をイベントとして通知します。このとき呼ばれるOthelloオブジェクトのメソッドをselectEventという名前にします。またこのメソッドの引数はクリックされた座標xとyにします。次にOthello.selectEventは、現在のターンのプレイヤーに対して、このイベントを通知します。Player側にも同じくselectEventという名前のメソッドを設けます。引数も先ほどと同じくxとyです。次にPlayer.selectEventは駒を置くという処理を入れます。駒を置く処理はOthelloオブジェクトのdoFlipとしてすでに定義されていましたね。これを使います。まとめてみるとこの処理に関連するメソッドは以下のようになります。

オブジェクト メソッド 役割 備考
View makeElement 各セルのDOM要素を作る 今回DOM要素に対してonclickというイベントを埋め込む。この中でOthelloオブジェクトのselectEventを呼ぶ
Othello selectEvent どこかのセルがクリックされたときに実行 現在のターンのプレイヤーのselectEventを呼ぶ
Player selectEvent どこかのセルがクリックされたときに実行 セルがクリックされたら、駒を置く。OthelloオブジェクトのdoFlipを実行。
Othello doFlip 駒を置く処理 大まかな処理は出来ているが、肝心な部分は未実装

ViewのmakeElementを改造する

makeElementでは、onclickイベントから、OthelloオブジェクトのselectEventを呼ぶという話をしました。しかしViewオブジェクトはOthelloオブジェクトの参照を持っていません。まずOthelloオブジェクトへの参照を行うプロパティとしてvar othelloを定義します。これはViewオブジェクトの外部からアクセスできないローカルなプロパティになります。次にViewに対してOthelloオブジェクトを登録するメソッドsetOthelloを設けます。これは外部からアクセス可能です。Othelloオブジェクトの初期化の部分から、こんな風にして使います。

//これはOthelloオブジェクトのinitメソッド内の処理
var board = new Board(); //Boardはクラスなのでこのようにしてオブジェクトを作る
var view = View; //Viewはそのまま使えるオブジェクト
view.setBoard(board);
view.setOthello(this); //this、つまりOthelloオブジェクト自身を登録

変更を加えた部分だけ書いておきます。

var View = {};
(function(){var othello;
	/**
	 * Othelloオブジェクトを登録(外部公開)
	 */
	function setOthello(_othello){
		othello = _othello;
	}
	View.setOthello = setOthello; //外部からアクセスできるようにする
	/**
	 * 指定された種類のセルのDOM要素を作成する(外部非公開)
	 * typeはblack, white, emptyのいずれか
	 */
	function makeElement(type,id,x,y){
		var element;
		if(type != "black" && type != "white" && type != "empty"){
			throw new Error("illegal argument 'type': "+type);
		}else{
	    	element = document.getElementById(type).cloneNode(true);
			if(!element)
				throw new Error("idが"+type+"のDOM要素をクローンできませんでした。");
		}
		element.style.left = 32 * x + "px";
		element.style.top = 32 * y + "px";
		//この部分を追加
		if(othello){ //othelloが定義されているなら
			(function(){
				var _x = x, _y = y, _othello = othello;
				element.onclick = function(){
					_othello.selectEvent(_x,_y);
				};	
			})();
		}
		element.id = id;
		return element;
	}
})();

ポイントは、onclickの部分でクロージャを使っていることです。このようにしないと、x、y、othelloの値がonclickに関連付けられた関数のなかで使えません。

OthelloオブジェクトにselectEventメソッドを作る

追加部分だけ書いときます

/**
 * ゲームのルールや流れを管理するオブジェクト
 */
var Othello = {};
(function(){/**
	 * Viewで発生したイベント(外部公開)
	 */
	 function selectEvent(x, y) {
		turn.selectEvent(x, y); //現在のプレイヤーのselectEventを呼ぶ
	}
	 Othello.selectEvent = selectEvent; //外部に公開
})();

PlayerクラスにselectEventメソッドを作る

PlayerのselectEventメソッドが呼ばれると、OthelloのdoFlipを呼ぶ必要があります。つまり、PlayerクラスはOthelloのオブジェクトを参照する必要があります。Viewオブジェクトの内部にvar othelloを作ったのと同様にPlayerクラスも内部にvar othelloと、それを設定するためのsetOthelloメソッドを作ります。追加点だけ書いておきます。

/**
 * プレイヤーを表すクラス
 */
var Player = function(piece, name) {var othello;
	/**
	 * Othelloオブジェクトを登録(外部公開)
	 */
	this.setOthello(_othello){
		othello = _othello;
	}
	
	/**
	 * セルがクリックされた時の処理
	 */
	this.selectEvent = function(x, y) {
		if(othello)
			othello.doFlip(this, piece, x, y);
	};

ここで定義したプレイヤーはOthelloオブジェクトのinitメソッドからこんな風にして使います。

//Othelloオブジェクトのinitメソッド内
var p1 = new Player(Piece.BLACK,"御坂美琴");
p1.setOthello(this);

これでViewで作られたセルのDOM要素がクリックされたら、プレイヤーからdoFlipが呼ばれるという処理の流れができました。

doFlipの話

doFlipの定義はもともと以下のようになっていました

function doFlip(player,piece,x,y){}

第一引数はプレイヤーのオブジェクト、第二引数は駒の種類、第三、四の引数は駒のx座標とy座標です。これと似たようなメソッドとして、駒が置けるかどうかチェックするcheckPieceがありました。checkPieceは以下のように定義されていました。

function checkPiece(piece,x,y){

比較してみると、doFlipだけはプレイヤーのオブジェクトを必要としているのが分かります。これは、現在のターンではないプレイヤーが駒を置くのを防ぐためです。現在のターンではないプレイヤーが勝手に駒を置けたら問題ですよね。例えば黒のプレイヤーが自分のターンでもないのに勝手に以下のように駒を置いたら問題です。

//黒のプレイヤーがターンを無視して勝手にコマを置く
doFlip(Piece.BLACK,0,0);
doFlip(Piece.BLACK,0,1);
doFlip(Piece.BLACK,0,2);

そこで第一引数でプレイヤー、つまりthisを渡しているわけです。

//黒のプレイヤーがターンを無視して勝手にコマを置く
doFlip(this,Piece.BLACK,0,0); //自分のターンじゃないので意味がない
doFlip(this,Piece.BLACK,0,1); //自分のターンじゃないので意味がない
doFlip(this,Piece.BLACK,0,2); //自分のターンじゃないので意味がない

ここでよく考えてみると、現在のdoFlipメソッドは駒を置く際に色を指定することができます。これも大きな問題です。これは自分の番でさえあれば、黒のプレイヤーが勝手に白の駒を置くことができるということを意味しています。

//黒のプレイヤーが勝手に白の駒を置いてる
doFlip(this,Piece.WHITE,0,0);

これを踏まえて、doFlipの定義を以下のように変更します。

function doFlip(player,x,y){}

具体的には以下のような実装になります。

var Othello = {};
(function(){/**
	 * 指定した座標に駒を置いた場合、 駒が何枚取れたのか返す(外部公開)
	 * 異常な場合はnullが
	 * 異常ではないものの、駒を取れない場合は0が返ってくる
	 */
	function doFlip(player,x,y){
		if(player != turn) //引数のプレイヤーが、現在のターンのプレイヤーでない場合エラー
			return;
		var piece = player.getPiece(); //プレイヤーから駒を判別
		var flip = checkPiece(piece, x, y); //ひっくり返せる駒の数
		if(flip == null || flip == undefined) //異常
			return;
		if(flip == 0) //駒を取れない
			return 0;
		//駒をひっくり返す処理を入れる(未実装)

		changeTurn(); //ひっくり返せたのでターン交代
		return flip;
	}
	Othello.doFlip = doFlip; //外部に公開
})();

内部でプレイヤーのgetPieceメソッドを使って、プレイヤーに対応した駒を取得しているのがポイントです。doFlipの定義を変更したので、これを使っていたプレイヤー側にも変更を加えます。変更部分だけ書いておきます。

/**
 * プレイヤーを表すクラス
 */
var Player = function(piece, name) {/**
	 * セルがクリックされた時の処理
	 */
	this.selectEvent = function(x, y) {
		if(othello)
			othello.doFlip(this, x, y); //ここだけ変わった
	};

ここまで出来たものを整理してみる

ここまでで作ってきたオブジェクトを整理してみます。各オブジェクトのプロパティとメソッドについて書いておきます。また、それらが外部からアクセス可能なのかどうかも書いておきます。

オブジェクト/クラス プロパティ/メソッド 公開しているか 役割
Piece BLACK 公開 黒の駒を表すオブジェクト
Piece WHITE 公開 白の駒を表すオブジェクト
Piece.BLACK/Piece.WHITE toString() 公開 駒のオブジェクトを文字列にする
Piece.BLACK/Piece.WHITE getOpposite() 公開 逆の駒のオブジェクトを返す
Boardクラス setPiece(piece,x,y) 公開 指定した位置に駒をセット
Boardクラス getPiece(x,y) 公開 指定した位置の駒を取得
View setBoard(board) 公開 boardオブジェクトをセット
View setOthello(othello) 公開 othelloオブジェクトをセット
View paint() 公開 オセロの盤を描写する
View makeElement(type,id,x,y) 非公開 各セルに対応するDOM要素を作る
Othello init() 公開 オセロのゲームを初期化
Othello changeTurn() 非公開 ターンを交代
Othello checkPiece(piece,x,y) 公開 指定したセルに駒を置くと、何枚取れるか
Othello doFlip(player,x,y) 公開 指定したセルに駒を置く
Othello endGame() 非公開 ゲーム終了、勝ち負け判定をする
Othello selectEvent(x,y) 公開 オセロの盤がクリックされると実行
Player toString() 公開 プレイヤーの名前を返す
Player getPiece() 公開 このプレイヤーの駒を返す
Player turn() 公開 ターンが回ってきたら呼ばれる
Player setOthello(othello) 公開 othelloオブジェクトをセット
Player selectEvent(x,y) 公開 オセロの盤がクリックされると実行

前回はスルーしたのですが、checkPieceとdoFlipの一部はまだ未実装になっています。実はこの部分が今回のプログラムで一番面倒くさかったりします。この部分が完成すれば、とりあえずオセロとして使うことができるはずです。次回はこの部分を実装していきます。最後にここまでのソースコードを添付しておきます。
oop-othello7.zip 直

追記 2010/01/09

添付したファイルのOthelloオブジェクトのcheckPieceメソッドとPlayerのturnメソッドにバグが見つかりました。ここではあまり重要ではありませんので、後の回で直します。申し訳ありません。