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

続きです

おおざっぱな設定

今回は前回作ったBoardクラスからboardオブジェクトを生成し、それを受け取って、実際に盤として描写するための「オセロの表示」に対応するViewオブジェクトを作っていきます。Boardオブジェクトは複数生成する必要があったのでクラスオブジェクトとして設計しましたが、Viewオブジェクトは1つで十分なのでそのまま使えるオブジェクトとして設計します。描写を行うということで、paintという名前のメソッドを設けます。次に描写を行うためにはBoardクラスから生成したオブジェクトが必要になります。このBoardから生成したオブジェクト(以下board)を設定するためのメソッドとして、setBoardというメソッドを設けます。paintメソッドではセットされたboardのgetPiece(x,y)というメソッドを使い、盤の状態を調べて、実際に目に見える形で描写を行います。

DOMの操作

ここでのポイントはpaintという機能をどのように実装するかでしょう。今回、一時間でオセロを作ってみたをおおいに参考にさせていただいて、作りました。基本となる部分は丸まるパクリともいえます。さて実際に描写を行うということで、htmlのDOMを操作する必要があります。まず、このスクリプトを実行するhtmlに対して、以下のようなdiv要素を設けます。

<div id="othello">
	<div id="board" style="position:relative;width:257px;height:257px"></div>
	<div style="display:none">
	  <div id="empty" class="empty" style="position:absolute;width:33px;height:33px;background-color:#000">
	     <div style="position:relative;top:1px;left:1px;width:31px;height:31px;background-color:#00ee00"></div>
	  </div>
	  <div id="white" class="white" style="position:absolute;width:33px;height:33px;background-color:#000">
	    <img style="display:block;position:relative;top:1px;left:1px;width:31px;height:31px;background-color:#00ee00" src="white.png">
	  </div>
	  <div id="black" class="black" style="position:absolute;width:33px;height:33px;background-color:#000">
	    <img style="display:block;position:relative;top:1px;left:1px;width:31px;height:31px;background-color:#00ee00" src="black.png">
	  </div>
	</div>
</div>

上から見ていきます。まずidがothelloのdiv要素を作ります。そしてその中を見てみると、最初にidがboardというdiv要素があります。この要素は、盤を描写するときの目印となるもので、この中にセルのDOM要素をJavaScriptで追加していきます。盤の位置は相対です。サイズは幅が257px、高さが257pxとなります。

次に3行目、styleがdisplay:noneとなっている要素があります。要するにこのDOM要素以下の部分は実際には描写されません。この内側に、駒が置いてない場合の空のセル、黒の駒が置いてある場合のセル、白の駒が置いてある場合のセルの原型を作成します。後々、ここで定義したセルをコピーして利用します。以下のような形式になっています。

<div id="empty" class="empty" style="position:absolute;width:33px;height:33px;background-color:#000"></div>
<div id="white" class="white" style="position:absolute;width:33px;height:33px;background-color:#000"></div>
<div id="black" class="black" style="position:absolute;width:33px;height:33px;background-color:#000"></div>

この各セルは1つ1つが、枠線も含めて33*33のサイズになります。この各セルの周り1pxを枠線にしますので、駒を描くのは31*31の範囲になります。さて、オセロは8*8のセルが必要なので、単純に考えると一辺が33*8=264pxになってしまいます。しかし先ほど設定した盤のサイズは一辺257pxです。これは、後に枠線の左と上の部分を1pxずつ重なるように表示を行うからです。1px重なるということは1つのセルが33-1=32px。これが8セルなので32*8=256px。そして一番右端と下端の枠は重ねませんので256+1=257pxとなり、最初に設定した盤の大きさと一致します。

それぞれの要素にclass属性として、idと同じ名前がついていますが、これは今は無視してください。次にこの要素の位置positionとして、absolute、絶対座標が指定されています。絶対座標とは、ある基準点から考えた座標のことです。今回idがboardになっているdiv要素を基準点として利用します。ここで定義した各セルは後に枠線を1px重ねるという話を先にしました。DOM要素を重ねるには絶対座標での位置指定が必要になってくるというわけです。相対座標、絶対座標の話は少しややこしいものです。HTMLクイックリファレンスの解説を読むと分かりやすいかと思います。そして背景色が#000つまり黒色に設定されています。これはセルの枠線の色ですが、33*33の真っ黒なセルが作られてしまいますね。次に各セルのDOM要素のさらに内側にあるDOM要素を見てください。

<div style="position:relative;top:1px;left:1px;width:31px;height:31px;background-color:#00ee00"></div>

これが枠線の部分を取り除いたセルに相当します。オセロの盤の色は緑なので、背景色として#00ee00が設定されています。topとleftは1pxとってあり、これによって真っ黒なセルの上から、緑色を被せて、周囲に黒枠を作っています。

黒の駒が置いてあるセルと白の駒が置いてあるセルには、以下の部分でコマの画像を描写します。この画像は、それぞれ白い丸と黒い丸のpng画像になっています。なお、画像の背景は透過色にしてあり、背景色である緑色が透けて見えるようになっています。

<img style="display:block;position:relative;top:1px;left:1px;width:31px;height:31px;background-color:#00ee00" src="white.png">
<img style="display:block;position:relative;top:1px;left:1px;width:31px;height:31px;background-color:#00ee00" src="black.png">

この画像です。

←(白のコマ。背景も白なので分かりにくいですね)
これをふまえて、各セルの原型は以下のような感じになります。

JavaScriptでセルを複製する

上で書いたDOM要素をJavaScriptで操作していきます。

var board_element = document.getElementById("board");
for ( var y = 0; y < 8; y++) {
    for ( var x = 0; x < 8; x++) {
    	var element = document.getElementById("empty").cloneNode(true);
        element.style.left = 32 * x + "px";
        element.style.top = 32 * y + "px";
        var id = "cell" + (x + 1  + y * 8); 
       	element.id = id;
    	board_element.appendChild(element);
    }
}

まず、idがboardのdiv要素を取得します。次にfor文を二回重ねることで、8*8の各セルを処理します。forの内部では最初に、idがemptyのdiv要素をcloneNode()を使ってコピーします。引数はtrueになっていますが、これは子要素もコピーするかどうかという引数です。コピーされた要素は、最初は以下ような状態になり、elementに代入されます。なおidは引き継がれていません。コピーする前の原型は、idを見れば"empty","black","white"だったので、それぞれ空、黒、白のセルというのが判別できたのですが、コピーを行うとそれが消えてしまったので判別できなくなります。それを判別できるよう、classにも"empty","black","white"と指定しています。

<div class="empty" style="position:absolute;width:33px;height:33px;background-color:#000">
    <div style="position:relative;top:1px;left:1px;width:31px;height:31px;background-color:#00ee00"></div>
</div>

このJavaScriptではこの要素に対して、さらに手を加えます。座標の指定、idの決定、そして最後にここで作った要素をboard要素の子要素として付け加えています。まず、座標を指定している部分をご覧ください。

element.style.left = 32 * x + "px";
element.style.top = 32 * y + "px";

少し前の話になりますが、左と上の枠線を1px重ねるという話をしました。それがまさにこの部分の処理になっています。一番最初に作られるのはxが0、yが0の要素。idがboardのdiv要素を基準点として、左と上が(0,0)になります。この次に作られるのはxが0、yが1なので(0,1)になります。

var id = "cell" + (x + 1  + y * 8); 
element.id = id;

次にこの部分で各セルにつけるidを決定しています。(x,y) = (0,0)のとき、idは"cell1"になり、(x,y) = (0,1)のときにidは"cell2"になります。これが順番に"cell64"まで続きます。これを実行すると、次のような感じになります。

boardの状態を読み取って描写

上の例では単純に空のセルを描写しただけですが、実際には盤を現すオブジェクトboardを読み取って、その状態に応じた描写を行う必要があります。この部分の処理を作っていきます。

var board = new Board();
board.setPiece(Piece.WHITE, 3, 3);
board.setPiece(Piece.WHITE, 4, 4);
board.setPiece(Piece.BLACK, 3, 4);
board.setPiece(Piece.BLACK, 4, 3);

var board_element = document.getElementById("board");
for ( var y = 0; y < 8; y++) {
	for ( var x = 0; x < 8; x++) {
		var element;
		switch ( board.getPiece(x, y) ) {
		case Piece.BLACK:
			element = document.getElementById("black").cloneNode(true);
			break;
		case Piece.WHITE:
			element = document.getElementById("white").cloneNode(true);
			break;
		case Piece.EMPTY:
			element = document.getElementById("empty").cloneNode(true);
			break;
		}
		if(element){
			element.style.left = 32 * x + "px";
			element.style.top = 32 * y + "px";
			var id = "cell" + (x + 1  + y * 8); 
			element.id = id;
			board_element.appendChild(element);
		}
	}
}

上から見ていきます。最初はBoardクラスから新たにboardというオブジェクトを生成しています。次に生成したboardに対して、白と黒の駒を配置しています。これがオセロ開始時においてある中央のコマになります。次にforの中をみてみると、先ほどはなかったswitch文が追加されています。ここでboardから指定した座標にある駒を取得、その種類に応じてコピーするelementを変えているというわけです。これを実行すると、下の図のような感じになります。

View

ここでやったことを踏まえて、Viewオブジェクトを作ると以下のようになります。

var init = function(){
	var board = new Board();
	board.setPiece(Piece.WHITE, 3, 3);
	board.setPiece(Piece.WHITE, 4, 4);
	board.setPiece(Piece.BLACK, 3, 4);
	board.setPiece(Piece.BLACK, 4, 3);

	View.setBoard(board);
	View.paint();
};

var View = {
	/**
	 * Boardクラスから生成したboardオブジェクトをセットする
	 */
	setBoard : function(board){
		this._board = board;
	},
	/**
	 * boardの情報を実際に盤として描写するための処理
	 */
	paint : function() {
		if(!this._board)
			throw new Error("board is null or undefined. Call setBoard() method before paint.");
		var board_element = document.getElementById("board");
		board_element.innerHTML ="";	//一旦全て消す
		for ( var y = 0; y < 8; y++) {
		    for ( var x = 0; x < 8; x++) {
		    	var element;
		    	switch ( this._board.getPiece(x, y) ) {
				case Piece.BLACK:
					element = document.getElementById("black").cloneNode(true);
					break;
				case Piece.WHITE:
					element = document.getElementById("white").cloneNode(true);
					break;
				case Piece.EMPTY:
					element = document.getElementById("empty").cloneNode(true);
					break;
				}
				if(element){
			        element.style.left = 32 * x + "px";
			        element.style.top = 32 * y + "px";
			        var id = "cell" + (x + 1  + y * 8); 
			       	element.id = id;
			    	board_element.appendChild(element);				
				}
		    }
		}
	}
};

まずはinitをご覧ください。これを見ればViewオブジェクトの使い方が分かるかと思います。最初にBoardクラスからboardオブジェクトを生成し駒を配置。次にsetBoardメソッドを使って、Viewにboardを登録します。そしてpaintメソッドが呼ばれると、登録されたboardを元に描写を行います。なお、ここで登録するboardオブジェクトは、this._boardとして外部からアクセスすることができます。本来ならこれを外部からアクセスできないようにしたかったのですが、今回の場合そのように実装するのは難しいです。そこで「外部からアクセスしないで下さい」という意味を込めて、先頭にアンダーバー「_」をつけ、_boardとしています。

次にViewオブジェクトのpaintメソッドの定義をご覧ください。最初にboardが設定されていない可能性がありますので、エラーチェックを入れています。そして次に

board_element.innerHTML ="";	//一旦全て消す

という行が入っています。もしこの行が無ければ、paintメソッドが呼ばれるたびに、セルを表現するDOM要素が増えていってしまいます。最初は64個しかDOM要素がありませんが、128、192、256とどんどん増えていきます。しかも見た目は変化しないのでやっかいです。そこでpaintが呼ばれるたび毎回全てのセルを削除しています。

次回に続きます。