15 PUZZLE

20131107

日経ソフトウェア2013年12月号の92ページから掲載「HTML5でゲームを作ろう」で紹介されていた15パズルを作成してみました。

Canvas で制作したのは、これまで円グラフと折れ線グラフぐらい。
たまにはゲーム制作も楽しそうだな・・・と、これを試したくて、日経ソフトウェア(月刊誌)を初購入。

紙面を見てソースを打ち込んだので、入力ミスが多かったのですが、ブラウザ(Firefox)のWeb開発→デバッガを実行すると、おかしい部分を指摘してくれるので助かりました。

・・・「15-puzzle.js」を「15-pazzle.js」と保存していたり、「NUM_BLOCKS」を一ヶ所だけ「NUM_BLOOKS」と打ち込んでいたり、散々でしたが・・・何とか動きました。

動作確認後、ほんの少しだけ手を加えましたが、重要な部分は掲載ソースのままです。

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

変更箇所は、

  • 15-puzzule.htmlでは、cssファイルとjsファイルの場所を変更。
  • 15-puzzle.jsでは、画像を5つ用意し、それらをPICTUERS配列にいれ、HTML読み込み時とクリア後に、ランダム選択としました。
  • シャッフル回数は、40回だと隅々までシャッフルされない場合があったので、160回にしました。
  • 数字表示をプラス1しています。「-1」より、この方がなじみやすいかな?と想いまして・・・。「-1」(=空白ブロック)、「0~14」(=画像ブロック)を、「0」(=空白ブロック)、「1~15」(=画像ブロック)にしました。
  • 背景の画像が白だとわかりにくかったため、数字のフォントを「白色」から、「白色」+「黒色の縁取り」にしました。

用意した画像(600×600ピクセル)は以下の5つです。

<!doctype html>
<html lang="ja">
<head>
	<meta charset="utf-8">
	<title>15パズル</title>
	<meta name="viewport" content="width=600">
	<link rel="stylesheet" type="text/css" href="../css/15-puzzle.css">
	<script type="text/javascript" src="../js/15-puzzle.js">
	</script>
</head>
<body onload="init()">
	<h1>15 PUZZLE</h1>
	<canvas id="gameCanvas" width="600" height="600"></canvas>
</body>
</html>

// 各種設定
var PICTURES = ["nobana.jpg", "risu.jpg", "matterhorn.jpg", "beach.jpg", "foods.jpg"];
var PICTURE_URL = "nobana.jpg";	// 画像ファイルのURL
var BLOCK_W = 150;	// ブロックの幅
var BLOCK_H = 150;	// ブロックの高さ
var ROW_COUNT = 4;	// 列を何枚に切るか
var COL_COUNT = 4;	// 行を何枚に切るか
var NUM_BLOCKS = ROW_COUNT * COL_COUNT;
// 上下左右の相対座標を定義したもの
var UDLR = [[0,-1],[0,1],[-1,0],[1,0]];
// ゲーム全体で使う変数
var context, image;	// 描画用
var blocks = [];	// 各ブロックを管理する配列変数
var isLock;	// マウス操作をロックするかどうか

// 初期化処理
function init() {
	// 描画コンテキストの取得
	var canvas = document.getElementById("gameCanvas");
	if (!canvas.getContext) {
		alert("Canvasをサポートしていません。");
		return;
	}
	context = canvas.getContext("2d");
	// マウスイベントの設定
	canvas.onmousedown = mouseHandler;
	// 画像を選択
	selectImage();
};

// 画像をランダムに選択
function selectImage() {
	r = Math.floor(Math.random() * PICTURES.length);
	PICTURE_URL = PICTURES[r];
	// メイン画像を読み出す
	image = new Image();
	image.src = PICTURE_URL;
	image.onload = initGame;	// 読み込んだらゲームを初期化
}

// ゲームの初期化
function initGame() {
	isLock = true;	// ユーザ操作をロックする
	// パズルのブロックを作成する
	for (var i = 0; i < NUM_BLOCKS; i++) {
		blocks[i] = i;
	}
	// 末尾(右下)を空きブロックとする
	blocks[NUM_BLOCKS -1] = -1;
	drawPuzzle();	// 見本を表示する
	// 1秒後にシャッフルを開始する
	setTimeout(shufflePuzzle,1000);
}
// パズルの各ピースをシャッフルする
function shufflePuzzle() {
	var scount = 160;	// シャッフルする回数を指定
	var blank = NUM_BLOCKS - 1;	// 空きブロック位置
	// 一回のみシャッフルを行う関数
	var shuffle = function () {
		scount--;
		if (scount <= 0) {
			isLock = false;	// ゲーム開始
			return;
		}
		var r, px, py, no;
		while (1) {
			r = Math.floor(Math.random() * UDLR.length);
			px = getCol(blank) + UDLR[r][0];
			py = getRow(blank) + UDLR[r][1];
			if (px < 0 || px >= COL_COUNT) continue;
			if (py < 0 || py >= ROW_COUNT) continue;
			no = getIndex(px, py);
			break;
		}
		blocks[blank] = blocks[no]
		blocks[no] = -1;
		blank = no;
		drawPuzzle();
		setTimeout(shuffle, 10);
	};
	shuffle();
}
// パズルの画面を描画する
function drawPuzzle() {
	for (var i = 0; i < NUM_BLOCKS; i++) {
		// 描画先座標を計算
		var dx = (i % COL_COUNT) * BLOCK_W;
		var dy = Math.floor(i / COL_COUNT) * BLOCK_H;
		// 描画元座標を計算
		var no = blocks[i];
		if (no < 0) {	// 空きブロック
			context.fillStyle = "#0000FF";
			context.fillRect(dx, dy, BLOCK_W, BLOCK_H);
		} else {
			var sx = (no % COL_COUNT) * BLOCK_W;
			var sy = Math.floor(no / COL_COUNT) * BLOCK_H;
			// 画像の一部を切り取って描画
			context.drawImage(image, sx, sy, BLOCK_W, BLOCK_H, dx, dy, BLOCK_W, BLOCK_H);
		}
		// 描画の枠を表示
		context.beginPath();
		context.strokeStyle = "white";
		context.lineWidth = 3;
		context.rect(dx, dy, BLOCK_W, BLOCK_H);
		context.stroke();
		context.closePath();
		// ブロック番号を描画する
		context.fillStyle = "rgba(255, 255, 255, 0.8)";
		context.font = "bold 48px Arial";
		var cx = dx + (BLOCK_W - 40) / 2;
		var cy = dy + BLOCK_H /2;
		context.fillText((no+1), cx, cy);
		context.strokeStyle = "black";
		context.strokeText((no+1), cx, cy);
	}
}
// マウスで移動先をクリックした時の処理
function mouseHandler(t) {
	if (isLock) return;
	// タッチ座標の取得
	var px = t.offsetX, py = t.offsetY;
	if (px == undefined) {	// FireFox対策
		var p = t.currentTarget;
		px = t.layerX - p.offsetLeft;
		py = t.layerY - p.offsetTop;
	}
	// 何番目のピースを動かしたいのか計算する
	var px2 = Math.floor(px / BLOCK_W);
	var py2 = Math.floor(py / BLOCK_H);
	var no = getIndex(px2, py2);
	// 空白ブロックなら動かせない
	if (blocks[no] == -1) return;
	// 上下左右に動かせるブロックがあるか確認
	for (var i = 0; i < UDLR.length; i++) {
		var pt = UDLR[i];
		var xx = px2 + pt[0];
		var yy = py2 + pt[1];
		var no = getIndex(xx, yy);
		if (xx < 0 || xx >= COL_COUNT) continue;
		if (yy < 0 || yy >= ROW_COUNT) continue;
		if (blocks[no] == -1) {	// 移動可能か
			blocks[no] = blocks[getIndex(px2,py2)];
			blocks[getIndex(px2,py2)] = -1;
			drawPuzzle();
			checkClear();
			break;
		}
	}
}
// クリアしたかどうかチェックする
function checkClear() {
	var flag = true;
	for (var i = 0; i < (NUM_BLOCKS -1); i++) {
		if (blocks[i] != i) { flag = false; break; }
	}
	if (flag) {
		alert("ゲームクリアしました!");
		selectImage();	// 再度ゲームを実行
	}
}
// 列と行からブロック番号を調べる関数
function getIndex(col, row) {
	return row * COL_COUNT + col;
}
function getCol(no) { return no % COL_COUNT; }
function getRow(no) {
	return Math.floor(no / COL_COUNT);
}

* {
	padding:0;
	margin:0;
	text-align:center;
}

h1 {
	background-color:blue;
	color:white;
	font-size:20px;
	padding:4px;
}