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

続きです。今回は「オセロのルール」に該当するオブジェクトOthelloを作っていきます。

おおまかな仕様

オセロの基本的な流れは、黒のプレイヤーと白のプレイヤーに交互にターン(番)が回ってきます。Othelloオブジェクトは、このターンの流れを管理します。両方のプレイヤーに対して、ターンが回ってきますので、ターンが回ってきたらPlayerオブジェクトに対して「ターンが回ってきたよ」と通知してあげるのが、Othelloオブジェクトの仕事です。ターンが回ってきたときにPlayerオブジェクトのturnメソッドを呼び出すという実装にします。各プレイヤーはどこに駒を置くのかを決めます。Othelloオブジェクトにターンを切り替えるためのchangeTurn()というメソッドを設けます。そして、changeTurn()の中で、各Playerにターンが回ってきたことを通知する処理を書きます。

ターンが回ってきても、駒を置ける場所がない場合、そのプレイヤーのターンはパスされます。changeTurn()の内部では、駒を置けるかどうか調べる必要があります。具体的にはcheckPiece()というメソッドを新たに設けます。これは、あるセルに対して、駒を置けるかどうか調べるためのメソッドです。checkPiece()には引数として駒の種類と位置を渡します。例えば黒の駒を(2,3)の位置に置けるか調べるという感じです。そして、返り値として、ひっくり返すことのできる相手の駒の数を返します。例えば置くことができない場所なら返り値が0になります。また相手の駒を3枚ひっくり返せるなら3を返します。changeTurn()の内部では、駒を置く場所があるのかどうか調べるために、全てのセルに対してcheckPiece()を実行します。そして一つも置ける場所がない場合は、ターンをスキップ、相手のターンになりますので、再びchangeTurn()を呼びます。

各プレイヤーを表すPlayerオブジェクトは駒を置く場所を自分で決めるという話を上でしました。しかしプレイヤーが選んだ場所が本当に駒を置いていい箇所なのかどうかは分かりません。また、置いていい場所だったら、実際に駒を置くわけですが、そこから相手の駒をひっくり返す処理が必要になります。このように実際に駒を置くという処理を行うためdoFlip()というメソッドを設けます。doFlip()には引数として、checkPiece()と同じく、駒の種類と位置を渡します。doFlip()は内部で、指定された位置に駒を置けるのかどうかcheckPiece()メソッドを呼び出します。そして問題がなければ、実際に駒を置き、相手の駒をひっくり返していきます。ひっくり返した後は、相手のターンになりますので、changeTurn()を呼び出します。このように、Othelloオブジェクトはかなり重要な役割をします。

最後にゲームの終了条件について考えてみます。ゲームが終わるのは、オセロの盤が64セル全て埋まる場合と、64セル全て埋まらないものの、両方のプレイヤーが置く場所がない場合の二つあります。共通していえるのは、64セルいっぱいになっている場合も、64セルいっぱいになってない場合も、置く場所がないということです。つまり両方のプレイヤーが駒を置く場所がない場合、そこでゲームが終了となります。これは二人のプレイヤーが連続してパスすることと同じです。この処理はchangeTurn()の内部で実現します。ゲームが終了した場合の処理として、endGame()というメソッドを新たに設けます。ここで、駒の数を数えて、最終的な勝ち負けを決定します。

今回、Othelloオブジェクトを1つ作ります。このOthelloオブジェクトを初期化するためinitというメソッドを設けます。またPlayerオブジェクトはクラスとして利用、ここからプレイヤーのオブジェクトを二人分作ります。整理すると次のような感じになります。

Othelloオブジェクト

メソッド 引数 返り値 役割 内部処理
changeTurn なし なし ターンを切り替える 最初に駒を置けるかチェック。置けるなら、現在のターンのプレイヤーと逆のプレイヤーのPlayerオブジェクトのturn()を呼ぶ。置けないならパスなので、もう一度changeTurn()を呼ぶ。二人のプレイヤーが続けてパスした場合、endGame()を呼ぶ。なおターン交代のタイミングは、Othelloオブジェクトの内部で決まるものなので、外部からはこのメソッドを見えないようにする
checkPiece 駒(piece)、駒を配置する座標(x,y) ひっくり返すことができる駒の数(0〜) 指定した座標に駒を配置した場合、相手の駒をいくつひっくり返せるか 指定した駒を中心として8方向に対してチェックを行う。
doFlip 駒(piece)、駒を配置する座標(x,y) ひっくり返した駒の数(0〜) 指定した座標に駒を配置し、相手の駒をいくつひっくり返したのか数を返す。 checkPiece()を実行し、その箇所に本当に駒が置けるのか調べる。駒が置けるなら、実際に駒をひっくり返し、最後にchangeTurn()を呼ぶ。
endGame なし なし ゲームを終了する 盤の上にある駒の数を集計し、勝ち負けを判定する。なおゲームの終了のタイミングは、Othelloオブジェクトの内部で決まるものなので、外部からはこのメソッドを見えないようにする。
init なし なし ゲームを初期化する Boardクラスからオブジェクトboardを生成する。中央の4セルに白と黒の駒を並べる。View.setBoardを呼んで、このboardを登録する。Playerクラスからプレイヤーを二人作成。先行のプレイヤーを設定。ゲームを開始


Playerクラス

メソッド 引数 返り値 役割 内部処理
turn なし なし プレイヤーのターンになると呼ばれる

順番に実装していく

/**
 * ゲームのルールや流れを管理するオブジェクト
 */
var Othello = {};
(function(){
	//外部に非公開な変数
	var board; //ボード
	var view; //ビュー
	var p1,p2; //プレイヤー
	var turn; //現在どちらのターンか
	/**
	 * 初期化
	 * Othello.initとして外部からアクセス可能
	 */
	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.setBoard(board);
		view.paint();
		p1 = new Player(Piece.BLACK,"御坂美琴");
		p2 = new Player(Piece.WHITE,"白井黒子");
		turn = p1; //先行
		alert("先行は"+turn+"("+turn.getPiece()+")です。"); //先行は御坂美琴(黒)です。
	}
	Othello.init = init;
})();

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

ここではOthelloオブジェクトとPlayerクラスを作成しました。両方ともクロージャのテクニックを利用しています。Othelloオブジェクトのinitメソッド内にある、以下の部分をご覧下さい。

p1 = new Player(Piece.BLACK,"御坂美琴");
p2 = new Player(Piece.WHITE,"白井黒子");
turn = p1; //先行
alert("先行は"+turn+"("+turn.getPiece()+")です。"); //先行は御坂美琴(黒)です。

ここではPlayerクラスから、二人のプレイヤーを作成しています*1。各Playerクラスから生成したオブジェクトは、toStringというメソッドを持っています。これにより、alert()のように文字列が必要なタイミングで、オブジェクトをそのまま記述すると、toStringが呼ばれ、nameの値が返されます。また、getPieceはPieceオブジェクトを返すものです。PieceオブジェクトもtoStringをオーバーライドで実装しており、文字列が必要なタイミングでオブジェクトを記述すると、駒の色が「黒」、「白」といった風に返されます。

骨組みだけ先に作る

処理の流れを実装してみました。

/**
 * ゲームのルールや流れを管理するオブジェクト
 */
var Othello = {};
(function(){
	var board; //ボード
	var view; //ビュー
	var p1,p2; //プレイヤー
	var turn; //現在どちらのターンか
	var skip; //連続でパスした回数
	/**
	 * 初期化(外部からアクセス可能)
	 */
	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.setBoard(board);
		view.paint();
		p1 = new Player(Piece.BLACK,"御坂美琴");
		p2 = new Player(Piece.WHITE,"白井黒子");
		turn = p1; //先行
		skip = 0;
		alert("先行は"+turn+"("+turn.getPiece()+")です。"); //先行は御坂美琴(黒)です。
		turn = (turn == p1 ? p2 : p1); //三項演算子 changeTurnを呼ぶと番が交代するので、あえてターンを逆にしている
		changeTurn();
	}
	Othello.init = init; //外部に公開
	/**
	 * ターン交代(外部から非公開)
	 * 置く場所があるかどうか調べ、あればプレイヤーのturn()メソッドを呼ぶ
	 * 無ければ、パスして、再びchangeTurn()
	 * 二回続けてパスされると、ゲーム終了
	 */
	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)){
					alert(turn+"("+piece+")の番");
					skip = 0;
					turn.turn(); //プレイヤーのturn()メソッドを呼ぶ
					return; //置く場所があればここで処理が終わる
				}	
			}
		}
		//置く場所が見つからない場合
		skip++;
		if(skip == 2){
			endGame();
		}else{
			alert(turn + "(" + piece + ")は置ける場所が無いのでパス");
			changeTurn();
		}
	}
	/**
	 * 指定した座標に駒を置いた場合、何枚取れるか返す(外部から公開)
	 */
	function checkPiece(piece,x,y) {
		if( board.getPiece(x,y) != Piece.EMPTY) //空のセル以外は禁止
			return 0;
		//この駒を中心に、8方向にチェックをかける
	}
	Othello.checkPiece = checkPiece; //外部に公開
	/**
	 * 指定した座標に駒を置いた場合、 駒が何枚取れたのか返す(外部から公開)
	 * 失敗した場合は0が返ってくる
	 */
	function doFlip(player,piece,x,y){
		if(player != turn) //引数のプレイヤーが、現在のターンのプレイヤーでない場合エラー
			return 0;
		var flip = checkPiece(piece, x, y);
		if(flip == 0)
			return 0;
		//駒をひっくり返す処理を入れる
		
		changeTurn(); //ひっくり返せたのでターン交代
		return flip;
	}
	Othello.doFlip = doFlip; //外部に公開
	/**
	 * ゲームを終了(外部から非公開)
	 * 勝ち負け判定を行う
	 */
	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++;
			}
		}
		alert("ゲームを終了します");
		//引き分け
		if(black == white){
			alert(black + " 対 " + white + "で引き分けです"); 			
		}else{ //どちらかが勝った
			var win = (black > white ? black : white);
			 //勝ったほうの色がp1の色ならp1の勝ち、そうでないならp2の勝ち 
			var winplayer = (win == p1.getPiece() ? p1 : p2);
			//例. 60 対 4で御坂美琴(黒)の勝ちです。
			alert(black + " 対 " + white + "で " + winplayer + "(" +win +")の勝ちです。"); 			
		}
	}
})();

最初におおまかな仕様として書いた文章のとおりに実装してみました。checkPieceメソッドとdoFlipメソッド以外はだいたい完成しています。ソース中にいっぱいコメント書いたので、詳しい解説はしません。頑張って読んでください。

ゲームの流れだけ書いておきます。最初に外部からOthello.init()と実行されます。initメソッドでは、最後にchangeTurnメソッドを呼びます。これによってゲームが開始されます。changeTurnでは、駒を置く場所がある場合、プレイヤーのturnメソッドを呼び出します。現状ではturnメソッドは何もしてくれません。また、何もしてくれなくてもかまいません。これは単にプレイヤーにターンが回ってきたというのを通知しているにすぎません。この部分については次回あたりにでも詳しく説明します。とりあえず、ゲームを進めるためには、プレイヤー側のオブジェクトからOthelloオブジェクトのdoFlipを呼ぶ必要があります。turnメソッドのことは忘れて、doFlipに頭を切り替えてください。プレイヤーは自分のターンになったら、駒を置く。これは、とても当たり前の処理ですよね。doFlipが実行されると、新しい駒が置かれ、相手の駒をひっくり返します。そして一番最後にchangeTurn()を呼んで相手の番になります。

再びchangeTurnメソッドの流れに戻ってきましたね。先ほどはchangeTurnメソッド内で、駒を置く場所がある場合について考えていました。次はどこにも駒を置けない場合を考えます。もし駒を置けない、要するにパスの状態が2回続いていればゲームは終了するので、endGameメソッドを実行します。1回目のパスであれば、changeTurnメソッドを呼んで、相手の番にしています。

とりあえず、ここまでのソースをまとめておきます。さて、checkPieceとdoFlipは未完成ですが、次回は先にPlayerオブジェクトについて考えてみます。

oop-othello6.zip 直

追記 2010/01/09

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

*1:どうでもいい話ですが、「白井黒子」ってオセロにふさわしい名前ですね!でも白か黒かはっきりして欲しいです。