オブジェクト指向っぽくオセロを作る(まとめ)

今までの内容のまとめです。

今までの記事の説明

追記 2009/1/20

新しい記事をかきました

また以下のURLで、このゲームを公開しました。
http://www17.atpages.jp/prime503/othello/

各オブジェクトのプロパティやメソッドについてみていきます

Pieceオブジェクト。「オセロの駒」を表現する。

プロパティ/メソッド 公開しているか 役割
BLACK 公開 黒の駒を表すオブジェクト
WHITE 公開 白の駒を表すオブジェクト
BLACK.toString()/WHITE.toString() 公開 駒のオブジェクトを文字列にする
BLACK.getOpposite()/WHITE.getOpposite() 公開 逆の駒のオブジェクトを返す

Boardクラス。「オセロの盤」を表現する。

プロパティ/メソッド 公開しているか 役割
setPiece(piece,x,y) 公開 指定した位置に駒をセット
getPiece(x,y) 公開 指定した位置の駒を取得

Viewオブジェクト。「オセロの表示」を表現する。

プロパティ/メソッド 公開しているか 役割
setBoard(board) 公開 boardオブジェクトをセット
setOthello(othello) 公開 othelloオブジェクトをセット
paint() 公開 オセロの盤を描写する
makeElement(type,id,x,y) 非公開 各セルに対応するDOM要素を作る
message(str,br) 公開 メッセージを表示する

Othelloオブジェクト。「オセロのルール」を表現する。

プロパティ/メソッド 公開しているか 役割
init() 公開 オセロのゲームを初期化
changeTurn() 非公開 ターンを交代
checkPiece(piece,x,y) 公開 指定したセルに駒を置くと、何枚取れるか
doFlip(player,x,y) 公開 指定したセルに駒を置く
endGame() 非公開 ゲーム終了、勝ち負け判定をする
selectEvent(x,y) 公開 オセロの盤がクリックされると実行
lineCheck(piece,x,y,dx,dy) 非公開 指定したセルに駒を置くと、指定した方向に対して何枚取れるか

Playerクラス。「プレイヤー」を表現する。

プロパティ/メソッド 公開しているか 役割
toString() 公開 プレイヤーの名前を返す
getPiece() 公開 このプレイヤーの駒を返す
turn() 公開 ターンが回ってきたら呼ばれる
setOthello(othello) 公開 othelloオブジェクトをセット
selectEvent(x,y) 公開 オセロの盤がクリックされると実行
getOthello() 公開 othelloオブジェクトをゲット

Computerクラス(Playerクラスを継承)。コンピューターの「プレイヤー」を表現する。

プロパティ/メソッド 公開しているか 役割
turn() 公開 スーパークラスのメソッドをオーバライド
selectEvent(x,y) 公開 スーパークラスのメソッドをオーバライド

ComputerExクラス(Computerクラスを継承)。コンピューターの「プレイヤー」を表現する。

プロパティ/メソッド 公開しているか 役割
turn() 公開 スーパークラスのメソッドをオーバライド

おおざっぱな流れ

Othello.init()でゲーム開始。プレイヤーやboardはここで生成される。初期化が終わるといよいよゲーム開始。最初はOthello.changeTurn()が呼ばれる。changeTurn()は駒が置けるなら、プレイヤーのturn()メソッドを呼ぶ。駒が置けないならパスとし、もう一度changeTurn()を呼ぶ。プレイヤー二人が連続してパスすると、二人とも置く場所がないということなので、ゲーム終了。Othello.endGame()を呼ぶ。

プレイヤーがコンピュータ、つまりComputerかComputerExの場合、ターンが回ってきて、turn()メソッドが呼ばれたら、駒を置く場所を捜してOthello.doFlip()を実行する。またプレイヤーが人間の場合は少し処理の流れが異なる。Viewで作成した各セルのDOM要素がクリックされたとき、Othello.selectEvent()が呼ばれる。ここで現在のターンを担当するプレイヤーのselectEvent()が呼ばれる。人間の場合、selectEvent()内部でOthello.doFlip()を実行する。

Othello.doFlip()が実行されると駒がひっくり返せるようであれば、ひっくり返していく。もし駒がひっくり返せたなら、Othello.changeTurn()を呼んで、ターン交代。

他に修正を加えられそうな箇所を考えてみます。

だいぶ拡張性のある設計になったのではないかと思います。今後実装するかは未定ですが、以下のような修正案が考えられます。

  • 数手先まで読んで駒を置くもっと賢いコンピューターを作る
  • 最後に置いた駒がどれなのか分かりやすいよう、ハイライトする
  • 初心者向けに駒を置ける箇所をハイライトする
  • 人間が駒を置いた後、コンピューターはすぐに駒を置くので、人間が駒を置いた場所が分かりにくい。これを解決するためにコンピューターが駒を置く処理を少し遅らせる。
  • サーバサイド技術と組み合わせてネットワーク対戦型にする
  • 駒の枚数を画面に表示する
  • 人間のプレイヤーは今まではマウスクリックで駒を置く場所を選んでいたが、キーボード操作によって、これを選択できるようにする。
  • CSSと組み合わせて簡単にオセロの見た目を変えられるようにする
  • 手戻りできるようにする(undo/redo)
  • ゲーム開始時にプレイヤーを選択できるようにする。例えば人間VS人間、人間VSコンピューター。

ひと段落つきました

今回PlayerとComputerを同一に扱えるあたりが特にオブジェクト指向ぽかったと思います。オセロの盤を、データ部分であるBoardと表示部分であるViewに分離したのはうまくいきました。それと、内部の処理で使うメソッドやプロパティを、頑張ってクロージャのテクニックを使って非公開にしたのもうまくいったと思います。これによって、オブジェクトの意図しない使われ方をだいぶ防げたと思います。

ここまでお付き合いいただいてありがとうございました。バグ報告や改善案、コメントをいただけると幸いです。

ソースコードを添付しておきます

こんな感じの画面になります。

以下のソースコードは、10回目の最後に添付したものと同じです。

oop-othello10.zip 直

index.html

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<script src="othello.js"></script>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Othello</title>
</head>
<body onload="init()">

<div id="othello">
	<div id="board" style="position:relative;width:257px;height:257px"></div>
	<div id="message"></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>

</body>
</html>

othello.js

var init = function(){
	Othello.init();
};

/**
 * 駒を表すオブジェクト(クラス)
 */
var Piece = function() {
	arguments.callee.prototype.toString = function() { // 文字列にする(外部公開)
		switch (this) {
		case Piece.BLACK:
			return "黒";
		case Piece.WHITE:
			return "白";
		}
		return "";
	};
	arguments.callee.prototype.getOpposite = function() { // 逆の色を返す(外部公開)
		switch (this) {
		case Piece.BLACK:
			return Piece.WHITE;
		case Piece.WHITE:
			return Piece.BLACK;
		}
		return null;
	};
};
Piece = { BLACK : new Piece(), WHITE : new Piece(), EMPTY : new Piece() };

/**
 * 盤を表すオブジェクト(クラス)
 */
var Board = function(){
	//8*8の二次元配列を作り、Piece.EMPTYで初期化
	var cells = new Array(8);
	for ( var x = 0; x < cells.length; x++) {
		cells[x] = new Array(8);
		for ( var y = 0; y < cells[x].length; y++) {
			cells[x][y] = Piece.EMPTY;
		}
	}
	// setter(外部公開)
	this.setPiece = function(piece, x, y) {
		if (!piece || !(0 <= x) || !(x <= 7) || !(0 <= y) || !(y <= 7))
			return false; // 引数チェック
		cells[x][y] = piece;
		return true;
	};
	// getter(外部公開)
	this.getPiece = function(x, y) {
		if (!(0 <= x) || !(x <= 7) || !(0 <= y) || !(y <= 7))
			return null; // 引数チェック
		return cells[x][y];
	};
};

var View = {};
(function(){
	var board;
	var othello;
	/**
	 * Boardクラスから生成したboardオブジェクトをセットする(外部公開)
	 */
	function setBoard(_board){
		board = _board;
	}
	View.setBoard = setBoard; //外部からアクセスできるようにする	
	/**
	 * Othelloオブジェクトを登録(外部公開)
	 */
	function setOthello(_othello){
		othello = _othello;
	}
	View.setOthello = setOthello; //外部からアクセスできるようにする
	/**
	 * boardの情報を実際に盤として描写するための処理(外部公開)
	 * 変更された箇所のみを再度描写しなおす
	 */	
	function paint() {
		if(!board)
			throw new Error("board is null/undefined. please call setBoard method before paint.");
		var board_element = document.getElementById("board");
		for ( var y = 0; y < 8; y++) {
		    for ( var x = 0; x < 8; x++) {
		    	/*
		    	 * boardオブジェクトが示す駒と
		    	 * DOM要素が示す盤の駒を比較
		    	 * 差異がある場合だけ描写を行う
		    	 */
		    	var nType; //boardオブジェクトの示す駒の種類
		    	switch ( board.getPiece(x, y) ) {
				case Piece.BLACK:
					nType="black";
					break;
				case Piece.WHITE:
					nType="white";
					break;
				case Piece.EMPTY:
					nType="empty";
					break;
				}
		    	//DOM要素の示す駒を取ってくる
		    	var id = "cell" + (x + 1  + y * 8); //idはcell1からcell64まで
		    	var bElement = document.getElementById(id);
		    	if(bElement){ //一番最初はDOM要素がないのでエラーではない
		    		var bType = bElement.className; //classは駒の種類に対応している
		    	}
		    	//同じなら描写しなおす必要がないので次の周に
		    	if(nType && bType == nType)
		    		continue;
		    	else{ //違うなら
		    		if(bElement) //なおかつ現在DOM要素があるなら
		    			board_element.removeChild(bElement); //DOM要素を取り除く
		    	}		    		
		    	//新たに追加するDOM要素を作る
		    	var nElement = makeElement(nType, id, x, y);
		    	if(nElement){
		    		board_element.appendChild(nElement);
		    	}
		    }
		}
	}
	View.paint = paint; //外部からアクセスできるようにする
	/**
	 * 指定された種類のセルの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;
	}
	/**
	 * メッセージを描写するための関数(外部公開)
	 * @str メッセージ
	 * @br 改行するかどうか.改行しない場合はfalse,改行する場合はtrueを渡す.デフォルトではtrueとなる.
	 */
	function message(str, br) {
		if (br == undefined) br = true;
		var mes_dom = document.getElementById("message");
		// memo. メッセージの数. 値を保存するためにmessageオブジェクトのプロパティとして定義
		arguments.callee.mes_num = arguments.callee.mes_num || 0;
		if (br) { //改行する場合
			arguments.callee.mes_num++;
			if (5 < arguments.callee.mes_num) { // メッセージの上限を5とし、それ以上になると古いものから削除
				mes_dom.removeChild(mes_dom.lastChild);
			}
			//最新のメッセージを一番上に追加
			mes_dom.innerHTML = "<div>" + str + "</div>" + mes_dom.innerHTML;
		} else { //改行しない場合
			var recent = mes_dom.firstChild;
			if (recent) { //最新のメッセージの最後尾につけたす
				recent.innerHTML += str;
			} else { //メッセージが一件もないということなので、改行をtrueにして再帰呼び出し.
				arguments.callee(str, true);
			}
		}
	}
	View.message = message; //外部からアクセスできるようにする
})();

/**
 * ゲームのルールや流れを管理するオブジェクト
 */
var Othello = {};
(function(){
	var board; //ボード
	var view; //ビュー
	var p1,p2; //プレイヤー
	var turn; //現在どちらのターンか
	var skip; //連続でパスした回数
	/**
	 * 初期化(外部公開)
	 * @return
	 */
	function init() {
		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 = View;
		view.setOthello(this);
		view.setBoard(board);
		view.paint();
		p1 = new Player(Piece.BLACK,"御坂美琴");
		p1.setOthello(this);
		//p2 = new Player(Piece.WHITE,"白井黒子");
		//p2 = new Computer(Piece.WHITE,"佐天涙子");
		p2 = new ComputerEx(Piece.WHITE,"初春飾利");
		p2.setOthello(this);
		turn = p1; //先行
		skip = 0;
		view.message("先行は"+turn+"("+turn.getPiece()+")です。"); //先行は御坂美琴(黒)です。
		turn = (turn == p1 ? p2 : p1); //三項演算子 changeTurnを呼ぶと番が交代するので、あえてターンを逆にしている
		changeTurn();
	}
	Othello.init = init; //外部に公開
	/**
	 * ターン交代(外部非公開)
	 * 置く場所があるかどうか調べ、あればプレイヤーのturn()メソッドを呼ぶ
	 * 無ければ、パスして、再びchangeTurn()
	 * 二回続けてパスされると、ゲーム終了
	 * @return
	 */
	function changeTurn() {
		//現在p1のターンならp2のターンに。そうでないならp1のターンにする。
		turn = (turn == p1 ? p2 : p1); //三項演算子
		//全セルから駒を置く場所があるか調べる
		for ( var y = 0; y < 8; y++) {
			for ( var x = 0; x < 8; x++) {
				var piece = turn.getPiece();
				if(0 < checkPiece(piece,x,y)){
					view.message(turn+"("+piece+")の番");
					skip = 0;
					turn.turn(); //プレイヤーのturn()メソッドを呼ぶ
					return; //置く場所があればここで処理が終わる
				}	
			}
		}
		//置く場所が見つからない場合
		skip++;
		if(skip == 2){
			endGame();
		}else{
			view.message(turn + "(" + piece + ")は置ける場所が無いのでパス");
			changeTurn();
		}
	}
	/**
	 * 指定した座標に駒を置いた場合、何枚取れるか返す(外部から公開)
	 * @param piece 駒の種類
	 * @param x
	 * @param y
	 * @return
	 */
	function checkPiece(piece,x,y) {
		var pieceNum = 0; // このコマを置くことによって、とることができるコマの数
		/*
		 * 8方向をチェック
		 * 指定した方向に対して、lineCheckメソッドを呼ぶ
		 * そのトータルの個数を最後に返す
		 */
		for ( var dy = -1; dy <= 1; dy++) { //dx,dyは次の方向
			for ( var dx = -1; dx <= 1; dx++) {
				var num = lineCheck(piece,x,y,dx,dy);
				if(0 < num)
					pieceNum += num;
			}
		}
		return pieceNum;
	}
	Othello.checkPiece = checkPiece; //外部に公開
	/**
	 * 指定した座標に駒を置いた場合、 駒が何枚取れたのか返す(外部公開)
	 * 駒を取れない場合は0が返ってくる
	 * @param player 駒を設置するプレイヤー
	 * @param x
	 * @param y
	 * @return		駒が何枚取れたのか
	 */
	function doFlip(player,x,y){
		if(player != turn) //引数のプレイヤーが、現在のターンのプレイヤーでない場合エラー
			return 0;
		var piece = player.getPiece();
		var oppositePiece = piece.getOpposite();
		var pieceNum = 0; //totalで取れる駒の枚数
		/*
		 * 駒をひっくり返す処理を入れる
		 * 最初にlineCheckを呼び、これによって取れる駒の数をチェック
		 * 1枚以上とれるなら、その方向で、相手の駒をひっくり返していく
		 */
		for ( var dy = -1; dy <= 1; dy++) { //dx,dyは次の方向
			for ( var dx = -1; dx <= 1; dx++) {
				var num = lineCheck(piece,x,y,dx,dy);// numは、特定の方向で取れるコマの数
				if(0 < num){
					pieceNum += num; //totalの個数をプラス
					nx = x + dx;
					ny = y + dy;
					nextPiece = board.getPiece(nx, ny);
					while (nextPiece == oppositePiece) {
						board.setPiece(piece, nx, ny); //駒をひっくり返す
						nx += dx;
						ny += dy;
						nextPiece = board.getPiece(nx, ny);
					}					
				}
			}
		}
		if(0 < pieceNum){ //もし駒がひっくり返せたら
			board.setPiece(piece,x,y); //最後に中央に駒を置く
			view.message("["+pieceNum + "個]",false);
			view.paint();
			changeTurn(); //ひっくり返せたのでターン交代
		}
		return pieceNum;
	}
	Othello.doFlip = doFlip; //外部に公開
	/**
	 *  (x,y)を中心としてdx,dy方向に駒をひっくり返せるかチェックする(外部非公開)
	 * @param piece		駒の種類
	 * @param x	0~7
	 * @param y	0~7
	 * @param dx		x方向(-1 ~ 1)
	 * @param dy		y方向(-1 ~ 1)
	 * @return	ひっくり返せる駒の枚数
	 */
	function lineCheck(piece,x,y,dx,dy){
		if( board.getPiece(x,y) != Piece.EMPTY) //空のセル以外は禁止
			return 0;
		if(piece != Piece.BLACK && piece != Piece.WHITE) //置けるのは黒か白のみ
			return 0;
		if(!(-1 <= dx && dx <= 1 && -1 <= dy && dy <= 1 ) || (dx == 0 && dy == 0))
			return 0;
		var oppositePiece = piece.getOpposite(); // 自分とは逆のコマ ex.黒なら白
		var num = 0; // numは、取れるコマの数
		var nx = x + dx; // nx, nyは次の座標
		var ny = y + dy; 
		var nextPiece = board.getPiece(nx, ny); //次の座標のコマ
		while ( nextPiece == oppositePiece) { // 逆のコマが続く間、進んでいく
			num++;
			nx += dx;
			ny += dy;
			nextPiece = board.getPiece(nx, ny);
		}
		//相手の駒が1つ以上あり、最後に自分の駒があれば
		if (0 < num && nextPiece == piece) {
			return num;
		}
		return 0;
	}
	/**
	 * ゲームを終了(外部非公開)
	 * 勝ち負け判定を行う
	 */
	function endGame() {
		//board上にある駒の数を数える
		var piece;
		var black=0,white=0; //得点
		for ( var y = 0; y < 8; y++) {
			for ( var x = 0; x < 8; x++) {
				piece = board.getPiece(x,y);
				if(piece == Piece.BLACK)
					black++;
				else if(piece == Piece.WHITE)
					white++;
			}
		}
		view.message("ゲームを終了します");
		//引き分け
		if(black == white){
			view.message(black + " 対 " + white + "で引き分けです"); 			
		}else{ //どちらかが勝った
			var win = (black > white ? Piece.BLACK : Piece.WHITE );
			 //勝ったほうの色がp1の色ならp1の勝ち、そうでないならp2の勝ち 
			var winplayer = (win == p1.getPiece() ? p1 : p2);
			//例. 60 対 4で御坂美琴(黒)の勝ちです。
			view.message(black + " 対 " + white + "で " + winplayer + "(" +win +")の勝ちです。"); 			
		}
	}
	/**
	 * Viewで発生したイベント(外部公開)
	 */
	 function selectEvent(x, y) {
		turn.selectEvent(x, y);
	}
	 Othello.selectEvent = selectEvent; //外部に公開
})();

/**
 * プレイヤーを表すクラス
 */
var Player = function(piece, name) {
	var othello;
	/**
	 * Othelloオブジェクトを登録(外部公開)
	 */
	this.setOthello = function(_othello){
		othello = _othello;
	};
	/**
	 * Othelloオブジェクトを登録(外部公開)
	 */
	this.getOthello = function(){
		return othello;
	};
	/**
	 * 名前を返す(外部公開)
	 */
	this.toString = function() {
		return name;
	};
	/**
	 * このプレイヤーの駒を返す(外部公開)
	 */
	this.getPiece = function() {
		return piece;
	};
	/**
	 * セルがクリックされた時の処理
	 */
	this.selectEvent = function(x, y) {
		if(othello)
			othello.doFlip(this, x, y);
	};
	/**
	 * ターンが回ってくると実行
	 */
	this.turn = function() {
	};
};

/**
 * コンピュータのプレイヤーを表すクラス
 */
var Computer = function(piece, name) {
	Player.call(this, piece, name);
	/**
	 * セルがクリックされた時の処理(外部公開)
	 * スーパークラスのメソッドをオーバライド
	 * 元の機能を削除している
	 */
	this.selectEvent = function(){};
	/**
	 * ターンが回ってくると実行(外部公開)
	 * スーパークラスのturnメソッドをオーバライド
	 */
	this.turn = function(){
		var othello = this.getOthello();
		for ( var y = 0; y < 8; y++) {
			for ( var x = 0; x < 8; x++) {
				if( 0 < othello.doFlip(this, x, y) ){ //片っ端から置いていく
					return; //置けたら終わり
				}
			}
		}
	};
};
Computer.prototype = new Player();
Computer.prototype.constructor = Computer;

/**
 * コンピュータのプレイヤーを表すクラス
 * Computerを継承
 */
var ComputerEx = function(piece, name) {
	Computer.call(this, piece, name);
	/**
	 * ターンが回ってくると実行(外部公開)
	 * スーパークラスのturnメソッドをオーバライド
	 */
	this.turn = function(){
		var othello = this.getOthello();
		var maxNum = 0; //一番たくさんとれる駒の数
		var maxX = -1;
		var maxY = -1; //そのときの座標
		for ( var y = 0; y < 8; y++) {
			for ( var x = 0; x < 8; x++) {
				var num = othello.checkPiece(piece,x,y);
				if(maxNum < num){
					maxNum = num;
					maxX = x;
					maxY = y;
				}
			}
		}
		if(0 < maxNum)
			if( othello.doFlip(this, maxX, maxY) != maxNum)
				throw new Error("ComputerEx内でエラーが発生しました");
	};
};
ComputerEx.prototype = new Computer();
ComputerEx.prototype.constructor = ComputerEx;