日経ソフトウェア2014年5月号「HTML5でゲームを作ろう(第6回)」に掲載された「落ち物パズル」です。
右の画像(タブレットで表示した画像)をクリックすると実際のゲームページを表示します。
落ち物(ピース)は動物で、犬・ゾウ・サル・猫・パンダの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)
しかし、ボタンの大きさも縮小され、やや左寄りに配置されてしまいます。(センタリングが崩れる)
・・・納得いかないので、スマートフォン用のページも用意しました。
1ピースの縦横幅を40pxに縮小してpng画像を再生成。40×8=320pxなので、所有スマートフォンでは見た目が良くなりました。
右の画像をクリックすると、スマートフォン用のゲームページを表示します。
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); }