セイムゲームのソースコード

セイムゲーム作ったよ」で紹介した奴のソースコードです。最初にソースコードを添付しておきます(後に掲載しているものと同じです)。
samegame.zip 直

方針

以前オセロを作ったのですが、オブジェクト指向的に書くということを目的にしていたので、少し大げさな仕様になってしまいました。今回の方針として、逆にあっさりとゲームを書こうとしたらどんな実装になるのかと思い作ってみました。

処理の流れ解説

最初にinit()という関数が呼ばれ、必要なDOM要素が作成されます。次にpaint()が呼ばれ、実際に画面にブロックが表示されます。あとは、ブロックをクリックした際に、onclickのイベントで、selectEvent()を呼びます。selectEventが実行されると、その内部で次はブロックを削除するためにcheck()を呼びます。ブロックを削除した後はブロックを落としてくる必要があるので、downBlockSequence()を呼びます。downBlockSequenceの中で、下側にブロックを落とす処理moveBottom()と、左側にブロックをつめる処理moveLeft()を呼びます。downBlockSequenceが終わると、最後にゲームの終了判定をcheckEndGame()というメソッドで行います。

このゲームには消しゴムというアイテムがあります。消しゴムをクリックした場合、ゲームの状態を保持するstatusという変数が"kesigom"という値になります。通常この値は"normal"という値を示しているのですが、"kesigom"という値のときに、ブロックがクリックされると、普段とは違う処理が行われます。またゲーム終了状態を示す"end"という値も取ります。この状態の場合、ブロックがクリックされても何もしません。

アルゴリズム

少し複雑なアルゴリズムをcheck()とcheckRecursive()に取り入れています。これは繋がっているブロックを探して消去するものです。このアルゴリズムについては、別の記事で解説しています。

次に、moveBottom()とmoveLeft()にも、check()ほどではありませんが、少し分かりにくいアルゴリズムを導入しています。これは配列の隙間をつめるためのアルゴリズムで、アルゴリズム教室を参考にさせていただきました。

ソースコード

短いものから載せていきます。まずindex.html

<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>セイムゲーム</title>
<script src="animation.js"></script>
<script src="samegame.js"></script>
</head>
<body onload="init()" style="padding:20px">
	<div id ="samegame"></div>
</body>
</html>

次にちょっとしたライブラリ。animation.js。DOM要素を作成します。Animationという名前がついてるけど、特にアニメーションをするわけではないです。

var Animation = {
	/**
	 * キャンバスを作成する
	 * @param {Object} x
	 * @param {Object} y
	 * @param {Object} width
	 */
	createCanvas : function(w,h,id,style){
		var element = document.createElement("div");
		if(id != null) element.id = id;
		if(w != null) element.style.width = w + "px";
		if(h != null) element.style.height = h + "px";
		element.style.overflow = "hidden";
		element.style.position = "relative";
		if(style != null) element.style.cssText += ";"+style;
		return element;
	},
	createCanvasObject : function(w,h,id,img,style){
		var element = document.createElement("div");
		if(id != null) element.id = id;
		if(w != null) element.style.width = w + "px";
		if(h != null) element.style.height = h + "px";
		element.style.overflow = "hidden";		
		element.style.position = "absolute";
		if(style != null) element.style.cssText += ";"+style;
		if (img != null) {
			var c_img = document.createElement("img");
			c_img.src = img;
			element.appendChild(c_img);
		}
		return element;
	}
};

最後に本体。正直あまり綺麗な処理ではないです。

/**
 * セイムゲーム
 * http://d.hatena.ne.jp/prime503/
 */

var canvas; //ゲームを描写するDOM要素
var least_element; //消しゴムの残り回数を表示するDOM要素
var score_element; //スコアを表示するDOM要素
var cells; //セルを表す二次元配列
var score; //得点
var status; //ゲームの状態
var kesigom_least; //消しゴムが使える回数
var CELL_X = 15;
var CELL_Y = 10;
var CELL_W = 60;

Type = function(mark,color){
	this.mark = mark;
	this.color = color;
	Type.prototype.validate = function(){
        for (var p in Type) {
            if (Type[p] == this){
               return true;
            }
        }
		return false;		
	};
};
Type = {
	BLOCK1 : new Type("■","blue"),
	BLOCK2 : new Type("●","skyblue"),
	BLOCK3 : new Type("▲","green"),
	BLOCK4 : new Type("★","orange"),
	BLOCK5 : new Type("◆","gray"),
	BLOCK6 : new Type("▼","red"),
	EMPTY : new Type()
};

function init(){
	score = 0;
	status = "normal";
	kesigom_least = 3;
	var samegameElement = document.getElementById("samegame");
	samegameElement.innerHTML ="";
	samegameElement.style.position="relative";
	//15*10のセルを作る
	cells = new Array(CELL_X);
	for (var i = 0; i < cells.length; i++) {
		cells[i] = new Array(CELL_Y);
		for (var j = 0; j < cells[i].length; j++) {
			var random = Math.floor(Math.random() * 6 + 1);
			switch (random) {
				case 1:
					cells[i][j] = Type.BLOCK1;
					break;
				case 2:
					cells[i][j] = Type.BLOCK2;
					break;
				case 3:
					cells[i][j] = Type.BLOCK3;
					break;
				case 4:
					cells[i][j] = Type.BLOCK4;
					break;
				case 5:
					cells[i][j] = Type.BLOCK5;
					break;
				case 6:
					cells[i][j] = Type.BLOCK6;
					break;					
			}
		}
	}
	canvas = Animation.createCanvas(CELL_X * CELL_W, CELL_Y * CELL_W, "canvas","background-color:#ddd;");
	samegameElement.appendChild(canvas);
	//消しゴム
	var kesigom = Animation.createCanvasObject(null,null,"kesigom","kesi.png","left:20px;top:20px;position:relative;overflow:visible;float:left;");
	kesigom.onclick=function(){
		itemEvent(ItemType.KESIGOM);
	};
	//消しゴムの残り回数を表示
	least_element = Animation.createCanvasObject(58,58,"least",null,"font-size:30px;font-weight:bold;color:red;overflow:visible;position:relative;"); //数字の部分
	kesigom.appendChild(least_element);
	samegameElement.appendChild(kesigom);
	
	//得点を表示する部分
	score_element = Animation.createCanvasObject(300,58,"score",null,"background-color:#ddd;font-size:50px;font-weight:bold;color:blue;text-align:center;position:relative;top:20px;left:200px;");
	samegameElement.appendChild(score_element);
	paint();
};

function paint(){
	canvas.innerHTML ="";
	for (var i = 0; i < cells.length; i++) {
		for (var j = 0; j < cells[i].length; j++) {
			var color = cells[i][j].color;
			var mark = cells[i][j].mark;
			if(color == null) continue;
			var block = Animation.createCanvasObject(CELL_W,CELL_W,"cell",null,
				"color:"+color+";font-size:"+(CELL_W-10)+"px;left:"+ (i * CELL_W) + "px;top:"+ j*CELL_W + "px;text-align:center");
			block.innerHTML =mark;
			(function(){
				var _i = i, _j = j;
				block.onclick = function(){
					selectEvent(_i, _j);
				};			
			})();
			canvas.appendChild(block);
		}
	}
	least_element.innerHTML="× " + kesigom_least;
	score_element.innerHTML= score;	

}

/**
 * ブロックが選択されたときの処理
 * @param {Object} x
 * @param {Object} y
 */
function selectEvent(x,y){
	if(status == "end")
		return;
	else if (status == "normal") {
		/**
	  * check関数で繋がっているブロックを探索
	  */
		//2つ以上繋がってるならブロックを消す
		var count = check(2, x, y);
		if (!(count >= 2)) //ブロックを消せないならここで終わり
			return;
		//ブロックをつめる
		downBlockSequence();
		//終了判定
		checkEndGame();
	}else if(status == "kesigom"){
		if (cells[x] == null || cells[x][y] == null || cells[x][y] == Type.EMPTY) {
		}else{
			cells[x][y] = Type.EMPTY;
			//ブロックをつめる
			downBlockSequence();
		}
		status = "normal"; //通常モードに戻す
	}
}
/**
 * 近接する同じ種類のブロックを探す
 * checkTypeの値を1に指定すると、近接するブロックの数を返す
 * checkTypeの値を2に指定すると、近接するブロックが2個以上の場合消去し、消去できた数を返す.
 * @param {Object}checkType チェックの種類
 * @param {Object} x ブロックを置く位置
 * @param {Object} y
 */
function check(checkType, x, y){
	if(cells[x] == null || cells[x][y] == null || cells[x][y] == Type.EMPTY ) return 0; //範囲外か空のブロック
	/*
	 * 探索用に二次元配列を作る.
	 * 未探索は-1
	 * 探索済み (x,y)にあるブロックと同じ種類のブロックは1
	 *               違う種類のブロック,空のブロックは 2
	 */
	var blockType = cells[x][y];
	var cells_check = new Array(CELL_X);
	for (var i = 0; i < cells_check.length; i++) {
		cells_check[i] = new Array(CELL_Y);
		for (var j = 0; j < cells_check[i].length; j++) {
			cells_check[i][j] = -1; //未チェック
		}
	}
	checkRecursive(x, y, cells_check, blockType);
	//隣接するブロックの数を数える
	var count = 0;
	for (var i = 0; i < cells_check.length; i++) {
		for (var j = 0; j < cells_check[i].length; j++) {
			if (cells_check[i][j] == 1) {
				count++;
			}
		}
	}
	//ブロック消去
	if (checkType == 2) {
		if (count >= 2) {
			score += count * 10;
			for (var i = 0; i < cells_check.length; i++) {
				for (var j = 0; j < cells_check[i].length; j++) {
					if (cells_check[i][j] == 1) {
						cells[i][j] = Type.EMPTY;
					}
				}
			}
		}else{ //消去失敗
			return 0;
		}
	}
	return count;
}

/**
 * @param {Object} x
 * @param {Object} y
 * @param {Object} cells_check 二次元配列
 * @param {Object} blockType ブロックの種類
 */
function checkRecursive(x, y, cells_check,blockType){
	if(cells[x] == null || cells[x][y] == null || 0 < cells_check[x][y]) return; //範囲外か探索済み
	if (cells[x][y] != blockType) { //ブロックの種類が違う
		cells_check[x][y] = 2;
		return;
	}
	//一致
	cells_check[x][y] = 1; //チェック
	checkRecursive( x + 1, y, cells_check, blockType);
	checkRecursive( x - 1, y, cells_check, blockType);
	checkRecursive( x, y + 1, cells_check, blockType);
	checkRecursive( x, y - 1, cells_check, blockType);	
	return;
}

/**
 * ブロックを詰めていく
 */
function downBlockSequence(){
		moveBottom(); //ブロックを下側に詰める
		paint();
		moveLeft(); //ブロックを左側に詰める
		setTimeout(paint, 250); //時間差を入れて描写	
}

/**
 * ブロックの下に隙間があれば詰める
 *
 * ■
 * ■
 * ×
 * ■
 *  
 * ↓
 * 
 * ■
 * ■
 * ■
 * 
 */
function moveBottom(){
    for (var i = 0; i < cells.length; i++) {
		/*
		 * 配列の隙間を詰めるアルゴリズム
		 * 参考 http://www.geocities.jp/oldbig_ancient/KodaiHP3.htm
		 */
		var pointer=cells[0].length - 1; //配列要素の移動先
        for (var j = cells[0].length - 1; j >= 0; j--) {
			var currentCell = cells[i][j];
			if(j != pointer && currentCell != Type.EMPTY){
				cells[i][pointer] = currentCell;
				cells[i][j] = Type.EMPTY;
			}
			if(currentCell.validate() && currentCell != Type.EMPTY){ //規定されているブロックなおかつ、空でない
				pointer--;
			}
        }
    }
}

/**
 * ブロックの左に列単位の隙間があれば列ごと左につめる
 *
 * ■
 * ■ ■
 * ■ ■
 * 
 * ↓
 * 
 * ■
 * ■■
 * ■■
 * 
 */
function moveLeft(){
	/*
	 * 配列の隙間を詰めるアルゴリズム
	 * 参考 http://www.geocities.jp/oldbig_ancient/KodaiHP3.htm
	 */
	var pointer=0; //配列要素の移動先
    for (var i = 0; i < cells.length; i++) {
		var currentCell = cells[i][cells[0].length - 1]; //一番下のブロック
		if (i != pointer && currentCell != Type.EMPTY) {
			//縦列ごと移動
			for (var k = 0; k < cells[0].length; k++) {
				cells[pointer][k] = cells[i][k];
				cells[i][k] = Type.EMPTY;
			}
		}
		if (currentCell.validate() && currentCell != Type.EMPTY) { //規定されているブロックなおかつ、空でない
			pointer++;
		}
	}
}

function checkEndGame(){
	//ゲームの終了判定.
	var isEnd = true;
	loop: for (var i = 0; i < cells.length; i++) {
		for (var j = 0; j < cells[0].length; j++) {
			if (check(1, i, j) >= 2) {
				isEnd = false;
				break loop;
			}
		}
	}
	if (isEnd) {
		setTimeout(function(){ //setTimeoutを使って、returnの後で実行
			status ="end";
			var last = 0; //残りブロックの数
			for (var i = 0; i < cells.length; i++) {
				for (var j = 0; j < cells[0].length; j++) {
					if (cells[i][j].validate() && cells[i][j] != Type.EMPTY) last++;
				}
			}
			if(last <= 10){ //残りブロックが10個以内ならボーナス
				var compl = 10 - last;
				var bonus = (compl + 50) * ( compl * 4 + 10); 
			}else{
				bonus = 0;
			}
			var kesi = kesigom_least * 300;
			var total = score + bonus + kesi;	
			var str ="ゲーム終了";
			str+= "\nブロック消去:" +score;
			str += "\n残りブロック ×"+last+" ボーナス:"+bonus;
			str += "\n残りアイテム ×"+kesigom_least+" ボーナス:"+kesi;
			str += "\n合計点:"+total;
			alert(str);
			score = total;
			paint();
			var endmessage = Animation.createCanvasObject(CELL_W*CELL_X,null,null,null,
				"color:black;font-size:200px;text-align:center;top:200px");
			endmessage.innerHTML="END";
			canvas.appendChild(endmessage);
		}, 250);
		return;
	}	
}

ItemType=function(){
	ItemType.prototype.validate = function(){
        for (var p in ItemType) {
            if (ItemType[p] == this){
               return true;
            }
        }
		return false;		
	};
};
ItemType={KESIGOM:new ItemType()};

function itemEvent(type){
	if(!type || !type.validate())
		throw new Erorr("itemEvent内でエラー");
	if(status == "end")
		return; //無視
	if(type == ItemType.KESIGOM && kesigom_least > 0){
		status ="kesigom";
		kesigom_least--;
		paint();
		
	}
}