落ち物パズル

日経ソフトウェア2014年5月号「HTML5でゲームを作ろう(第6回)」に掲載された「落ち物パズル」です。

20140328tablet

右の画像(タブレットで表示した画像)をクリックすると実際のゲームページを表示します。

落ち物(ピース)は動物で、犬・ゾウ・サル・猫・パンダの5種類です。(Inkscapeでお絵描き)

ピースは2個セットで落ちてきます。
プレーヤーは落下中に、カーソルキー(または下部ボタン)を押して、ピースを動かします。

「←」キー(または「←」ボタン)で左へ移動。
「↑」キー(または「@」ボタン)で90度右回転。
「→」キー(または「→」ボタン)で右へ移動。
「↓」キー(これはボタンが無い)で下へ移動。

ステージで同じ種類のピースが縦または横に3個以上並ぶと、それらのピースは消えます。
ピースが消えると上にあるピースが落下します。その際、同じ種類のピースが縦または横に3個以上並ぶと、それらのピースも消えます。(連鎖発生)

ソースコードはほぼオリジナルのままです。
紙面では、行数が7でしたが、ステージまでの距離が近くて、難易度が高く感じたので、10行に変更しました。
・・・ノートPC等で画面に収まりきらない場合はブラウザのズーム機能で縮小して下さい。

スマートフォン(幅360px)で表示すると、画面からはみ出してしまったので、htmlファイルのViewportの設定を変えました。
画面の幅は1ピースが縦横64pxなので、8列マスで、64×8=512px。
ページが最初に読み込まれるときの拡大率(initial-scale)を0.625倍にしました。(320÷512=0.625)
しかし、ボタンの大きさも縮小され、やや左寄りに配置されてしまいます。(センタリングが崩れる)

20140328sp

・・・納得いかないので、スマートフォン用のページも用意しました。
1ピースの縦横幅を40pxに縮小してpng画像を再生成。40×8=320pxなので、所有スマートフォンでは見た目が良くなりました。

右の画像をクリックすると、スマートフォン用のゲームページを表示します。

20140328c



PC・タブレット端末用画像(resource.png)



・・・初回カーソルキーが動作しない場合がありました。
・・・その場合は、一度ボタンをマウスでクリックすると動作しました。???
・・・点数のカウントが正確ではないような・・・。
・・・時々、別の種類のピースも巻き込んで消える事があるような・・・。

・・・まっ、細かいことは、気にしない、気にしない。

・・・やり始めると延々とやり続けてしまうような中毒性があるかも。

・・・ハマり過ぎて時間の無駄遣いにならないように御注意下さい。




ソース(PC・タブレット端末用)はこちら


<!DOCTYPE html>
<html>
	<head>
		<meta charset="UTF-8">
		<meta name="viewport" content="target-densitydpi=device-dpi, width=device-width; initial-scale=0.625; maximum-scale=1.0; user-scalable=0;">  
		<title>落ち物</title>
		<script type="text/javascript" src="ochimono.js"></script>
		<style>
			* {
				padding: 0px;
				margin: 0px;
				text-align: center;
			}
			h1 {
				padding: 8px;
				margin: 0px;
				font-size: 24px;
				background-color: white;
			}
			body {
				background-color: white;
			}
			button {
				height: 64px;
				width: 100px;
				font-size: 32px;
			}
		</style>
	</head>
	<body>
	<h1>落ち物<span id="info">0点</span></h1>
		<canvas id="aCanvas"></canvas><br>
		<button id="btnLeft">←</button>
		<button id="btnRotate">@</button>
		<button id="btnRight">→</button>
	</body>
</html>

// 定数宣言
var PIECE_W = 64;		// 1ピースの幅
var PIECE_H = 64;		// 1ピースの高
var STAGE_COLS = 8;	// ステージの列数
var STAGE_ROWS = 10;	// ステージの行数
var STAGE_W = STAGE_COLS * PIECE_W;
var STAGE_H = STAGE_ROWS * PIECE_H;
var FILE_RESOURCE = "resource.png";	// 画像ファイル
var PIECE_TYPES = 5;	// ピースの種類
var TURN_TIME = 300;	// 1ターンの時間
var DROP_TURN = 3;		// 3ターンに1度ピースが落下
var DROP_TIME = TURN_TIME * DROP_TURN;
// 上下左右の方向を表す配列
var URDL = [[-1,0],[0,1],[1,0],[0,-1]];
// 変数宣言
var aCanvas, ctx;	// キャンバスのオブジェクト
var imgChars;		// ピース画像用のImageオブジェクト
var px,py;		// 落下ピースの座標
var pc;			// 落下ピースの形状
var userLR, userRO, userDN;		// プレーヤーの操作状況
var stage = [];	// ステージ
var turnCount;	// 現在何ターン目かを数える
var score;		// スコア
// 初期化処理
window.onload = function () {
	// キャンバスの描画コンテキストの取得
	aCanvas = $("aCanvas");
	aCanvas.width = STAGE_W;
	aCanvas.height = STAGE_H;
	ctx = aCanvas.getContext("2d");
	// 画像の読み込み
	imgChars = new Image();
	imgChars.onload = newGame;
	imgChars.src = FILE_RESOURCE;
	// イベントハンドラの設定
	window.onkeydown = keyHandler;
	$("btnLeft").onmousedown = goLeft;
	$("btnLeft").ontouchstart = goLeft;
	$("btnRight").onmousedown = goRight;
	$("btnRight").ontouchstart = goRight;
	$("btnRotate").onmousedown = goRotate;
	$("btnRotate").ontouchstart = goRotate;
};
// 新規ゲームを開始する処理
function newGame() {
	initStage();
	px = py = -1; pa = [];
	userLR = userRO = userDN = 0;
	turnCount = score = 0;
	disp("0点");
	nextTurn();
}
// ステージ0で初期化する
function initStage() {
	stage = [];
	for (var y = 0; y < STAGE_ROWS; y++) {
		var n = stage[y] = [];
		for (var x = 0; x < STAGE_COLS; x++) {
			n[x] = 0;
		}
	}
}
// キーボードのイベントハンドラ
function keyHandler(e) {
	switch (e.keyCode) {
		case 37: userLR = -1; break;
		case 38: userRO = 1; break;
		case 39: userLR = 1; break;
		case 40: userDN = 1; break;
	}
}
// ターンを進める(ゲームループ)
function nextTurn() {
	checkUserInput();
	if (turnCount % DROP_TURN == 0) {
		if (!dropPiece()) return;
		gravity();		// 下に落ちるピースがあるか確認
	}
	drawStage();
	turnCount++;
	// 自分自身をTURN_TIME秒後に呼び出す
	setTimeout(nextTurn, TURN_TIME);
}
// プレーヤーの入力をチェックし、ピースを動かす
function checkUserInput() {
	if (py < 0) return;
	if (userLR != 0) {		// 左右移動
		var xx = userLR + px;
		if (checkBlank(py, xx)) {
			px = xx;
		}
		userLR = 0;
	}
	if (userDN > 0) {		// 下に移動
		var yy = py + 1;
		if (checkBlank(yy, px)) {
			py = yy;
		}
		userDN = 0;
	}
	if (userRO > 0) {		// 回転
		var pc2 = rotate(pc);	// 回転した形状を作る
		var tmp = pc;			// 回転前の形状を保存しておく
		pc = pc2;
		if (!checkBlank(py, px)) {		// 回転可能か?
			pc = tmp;		// 無理ならば回転前の形状に戻す
		}
		userRO = 0;
	}
}
// 落下中のペースを下に落とす
function dropPiece() {
	// 画面の落下中にピースがない場合は生成する
	if (py < 0) {
		pc = [[1 + rand(PIECE_TYPES), 0],
				 [1 + rand(PIECE_TYPES), 0]];
		if (rand(2)) pc = rotate(pc);
			px = rand(STAGE_COLS-2);
			py = 0;
			// ゲームオーバーの判定
			if (!checkBlank(py, px)) {
				drawStage();
				alert("Game Over");
				newGame();
				return false;
			}
		return true;
	}
	// 落下中のピースはさらに下に移動できるか?
	var r = checkBlank(py+1, px);
	if (!r) {	// 移動できないのでステージに定着
		iter2d(pc, function(y, x, c) {
			if (c == 0) return true;
			stage[px + x] = c;
			return true;
		});
		px = py = -1; pa = [];
	} else {
		py++;		// 落下する
	}
	return true;
}
// ピースの移動・回転可能性を検証する
function checkBlank(ty, tx) {
	var isBlank = true;
	iter2d(pc,function(y, x, c) {
		var xx = tx + x;
		var yy = ty + y;
		if (c == 0) return true;
		if (xx < 0 || yy < 0 ||
			 yy >= STAGE_ROWS ||
			 xx >= STAGE_COLS) {
			isBlank = false;
			return false;
		}
		var cc = stage[yy][xx];
		if (cc != 0) {
			isBlank = false;
			return false;
		}
		return true;
	});
	return isBlank;
}
// 上下か左右に3つ以上同じ種類のピースがあれば消す
function checkPoping() {
	var total = 0;
	iter2d(stage, function(y, x, c) {
		if (c == 0) return true;
		// 上下左右で同じ色の数を数える
		var cnt = countSameType(y,x,c);
		// 上下左右に3つ以上並んでいれば消す
		if (cnt >= 3) {
			popSameType(y, x, c);
			total += cnt;
		}
		return true;
	});
	if (total > 0) {
		score += total * 2;
		disp(score+"点");
	}
}
// 同じ種類のピースを数える
function countSameType(y, x, c) {
	var count = 0;
	for (var i in URDL) {
		var cnt = 0;
		var dir = URDL[i];
		var yy = y, xx = x;
		for (;;) {
			if (yy < 0 || yy >= STAGE_ROWS ||
				 xx < 0 || xx >= STAGE_COLS) break;
			if (c == 0 || c != stage[yy][xx]) break;
			cnt++;
			if (cnt >= 3) count += cnt;
			yy += dir[0];
			xx += dir[1];
		}
	}
	return count;
}

// 同じ種類のピースを消す
function popSameType(y, x, c) {
	if (c == 0) return 0;
	if (y < 0 || y >= STAGE_ROWS) return 0;
	if (x < 0 || x >= STAGE_COLS) return 0;
	if (c != stage[y][x]) return 0;
	stage[y][x] = 0; 	// 消す
	var count = 1;
	// 上下左右を確認する
	for(var i = 0; i < URDL.length; i++) {
		var dir = URDL[i];
		var yy = y + dir[0];
		var xx = x + dir[1];
		var cnt = popSameType(yy,xx,c);
		count += cnt;
	}
	return count;
}
// ピースが消えたとき、上にあるピースを重力で落とす
function gravity() {
	var count = 0;
	for (var y = 1; y < STAGE_ROWS; y++) {
		var row = STAGE_ROWS - y - 1;
		for (var x = 0; x < STAGE_COLS; x++) {
			var c = stage[row][x];
			if (c == 0) continue;
			if (stage[row+1][x] == 0) {	// 下に落とす
				stage[row+1][x] = c;
				stage[row][x] = 0;
				count++;
			}
		}
	}
	// 再度checkPoping関数を呼んで連鎖を試す
	setTimeout(checkPoping, DROP_TIME);
}
// ステージを描画する
function drawStage() {
	ctx.clearRect(0,0,STAGE_W,STAGE_H);
	// 定着したピースの描画
	iter2d(stage, function(y,x,c) {
		var xx = x * PIECE_W;
		var yy = y * PIECE_H;
		if (c == 0) {
			ctx.strokeStyle = "gray";
			ctx.strokeRect(xx, yy, PIECE_W, PIECE_H);
			return true;
		}
		drawChar(xx, yy, c);
		return true;
	});
	// 落下中のピースを描画
	iter2d(pc, function(y, x, c) {
		if (c == 0) return true;
		var xx = (px + x) * PIECE_W;
		var yy = (py + y) * PIECE_H;
		drawChar(xx, yy, c);
		return true;
	});
}
//ピースの画像を(x, y)に描画する
function drawChar(x, y, c) {
	ctx.drawImage(imgChars,
		(c - 1) * PIECE_W, 0,
		PIECE_W, PIECE_H,
		x, y, PIECE_W, PIECE_H);
}
// 整数の乱数を返す関数
function rand(n) { return Math.floor(Math.random()*n); }
// 2×2の2次元配列を右回転させる
function rotate(p) {
	var r = [ [ p[1][0], p[0][0] ],
					[ p[1][1], p[0][1] ] ];
	return r;
}
// 2次元配列を巡回する(イテレータ関数)
function iter2d(tbl, callback) {
	for (var y = 0; y < tbl.length; y++) {
		var cells = tbl[y];
		for (var x = 0; x < cells.length; x++) {
			var r = callback(y, x, cells[x]);
			if (!r) return;
		}
	}
}
// ボタンのイベントハンドラの実装
function goLeft(e) { e.preventDefault(); userLR = -1; }
function goRight(e) { e.preventDefault(); userLR = 1; }
function goRotate(e) { e.preventDefault(); userRO= 1; }
// スコアを画面に描画する
function disp(s) { $("info").innerHTML = s; }
// DOM要素セレクタ
function $(id) { return document.getElementById(id); }