8年ほど前に投稿した箱入り娘パズルですが、今年9月に文字をイラストに置き換えたVer.1.01を掲載。
これまで、駒の移動は最小駒二個分の隙間が空いていても、最小駒の単位で動かす必要がありました。
(その為、手数のカウントも増えていました。)
今回、2クリックで移動していたところを、1クリックで移動可能としました。
以下の画像をクリックすると実際のゲームを表示します。
パズルで使用している画像は2つ。
■背景の画像(back.png)・・・こちらは前作と同じ。
■パズルで使用している画像(resource_102.png)・・・今回は枠部分のみ改修しました。駒には手を加えていません。
今回も、GIMPで適当に作成。
だいぶ汚れてきたソースを以下に掲載します。
hakoiri_102.html
- (7行目)
ヘッダ内で「hakoiri_102.js」を指定しています。 - (10行目)
背景画像「back.png」を指定しています。
<! DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, height=480, initial-scale=0.5, minimum-scale=0.5, maximum-scale=2.0, user-scalable=no" />
<title>箱入り娘</title>
<script type="text/javascript" src="hakoiri_102.js"></script>
<style>
* { margin:0; padding:0; text-align:center; }
body { background-image: url('back.png'); }
h1 { background-color: #cccc99; color:white; padding:7px; font-size:1.5em; }
</style>
</head>
<body>
<div id="top_title"></div>
<!--<h1>箱入り娘</h1>-->
<div id="info">-</div>
<canvas id="aCanvas"></canvas>
</body>
</html>
hakoiri_102.js
- (7行目)
パズル用画像「resource_102.png」を指定しています。 - (82~83行目)
これまで、初回に駒ではなく隙間部分を選択できていたバグを改修。 - (119行目)
デバックの為のログ表示。先頭のコメント(//)を消去するとブラウザのデベロッパーツール「Console」に情報が表示されます。 - (120~152行目)
最小駒[1,1]と縦長駒[1,2]&横長駒[2,1]の「縦2駒移動」あるいは「横2駒移動」、最小駒[1,1]の「斜め2駒移動」(右上、右下、左下、左上)、3駒以上先への移動禁止、縦長駒[1,2]&横長駒[2,1]の「斜め移動禁止」などを、if文を多用し、スマートさ皆無で追加。
// file: hakoiri_102.js
// --- 定数の宣言 ---
var CW = 100; // 駒の最小幅
var COLS = 6, ROWS = 7;
var CANVAS_W = CW * COLS;
var CANVAS_H = CW * ROWS;
var RESOURCE_FILE = "resource_102.png";
// 隙間と駒、 箱の枠を定義
var __=0,DT=1,K2=2,K3=3,K4=4,K5=5;
var K6=6,K7=7,K8=8,K9=9,KA=10,XX=11;
// 隙間と駒、 箱の枠のサイズを定義
var CH_SIZE = [
[1,1],[2,2],[1,2],[1,2],[1,2],[1,2],
[2,1],[1,1],[1,1],[1,1],[1,1],[1,1]];
// resource.png上の隙間と駒、箱の立置を定義
var CH_POS = [0,1,3,4,5,6,7,9,10,11,12,13];
// 箱の中(ステ一ジ)の初期状態を定義
var DEF_STAGE = [
[XX,XX,XX,XX,XX,XX],
[XX,K2,DT,DT,K3,XX],
[XX,K2,DT,DT,K3,XX],
[XX,K4,K6,K6,K5,XX],
[XX,K4,K7,K8,K5,XX],
[XX,K9,__,__,KA,XX],
[XX,XX,XX,XX,XX,XX]
];
// --- 大域変数の定義 ---
var play_msg = "<h1>箱入り娘</h1>"; // PUZZLEのタイトル
var clear_msg = "<h1>箱入り娘 CLEAR !</h1>"; // PUZZLE クリア時のタイトル
var aCanvas, ctx; // キヤンバスとコンテキスト
var images; // 画像オブジエク卜
var stage; // ステ一シを表す変数
var turn; // 手数
var sel_x, sel_y, sel_no; // 選択中の駒情報
var t_out = 15; //クリア後のタイムアウト秒数
// --- 初期化イべント ---
window.onload = function() {
// キヤンバスのサイズをセット
aCanvas = $("aCanvas");
aCanvas.width = CANVAS_W;
aCanvas.height = CANVAS_H;
// キヤンバスのイべン卜をセット
aCanvas.onclick = clickHandler;
// 描画コンテキス卜の取得
ctx = aCanvas.getContext("2d");
// リソース画像を読み込む
$("info").innerHTML = " 少々お待ちください";
images = new Image();
images.src = RESOURCE_FILE;
images.onload = initGame;
};
// ゲームのバラメー夕を初期化
function initGame() {
document.getElementById('top_title').innerHTML = play_msg;
$("info").innerHTML = "-";
stage = cloneArray(DEF_STAGE);
turn = 0;
sel_x = sel_y = sel_no = -1; // 未選択
drawScreen();
}
// --- マウスイベント ---
function clickHandler(e) {
var pt = getClientPos(e);
var x = Math.floor(pt.x / CW);
var y = Math.floor(pt.y / CW);
clickstage(x, y);
}
// --- マウス座標を変換する ---
function getClientPos(e) {
var res = { x:0, y:0 };
var rect = e.target.getBoundingClientRect();
res.x = e.clientX - rect.left;
res.y = e.clientY - rect.top;
return res;
}
// --- 駒の移動処理 ---
// (x,y)をクリックした時の処理
function clickstage(x, y) {
var no = stage[y][x];
console.log("click=" + no);
// クリックしたのが枠なら何もしない
// 初回クリックが隙間なら何もしない
if ((no == XX) || (sel_no <= 0 && no == __)) return;
// 選択状態で隙間を選択した場合
if (no == __ && sel_no > 0) {
checkMove(x, y);
return;
}
// 選択座標を記録する
sel_x = x; sel_y = y; sel_no = no;
// 選択座標を補正。娘の駒なら必ず左上になる
if (CH_SIZE[no][0] > 0) {
if (stage[y][x-1] == no) sel_x--;
}
if (CH_SIZE[no][1] > 0) {
if (stage[y-1][x] == no) sel_y--;
}
$("info").innerHTML = "移動方向をクリックしてください。";
drawScreen();
}
// 移動可能か判定
function checkMove(x, y) {
// 移動先が隙間かを確かめる
// ただし右か下に移勤する場合を考慮
var sel_w = CH_SIZE[sel_no][0];
var sel_h = CH_SIZE[sel_no][1];
if (sel_w >= 2 && sel_x < x) x -= sel_w-1;
if (sel_h >= 2 && sel_y < y) y -= sel_h-1;
var res = loop2(sel_h, sel_w, function(e) {
var nx = e.x + x;
var ny = e.y + y;
var v = stage[ny][nx];
if (v != __ && v != sel_no) {
e.stop = true;
}
});
if (!res) return;
// 駒が隣り合っているかを判定する
// console.log("test039 sel_x="+sel_x+" sel_y="+sel_y+" x="+x+" y="+y);
if (sel_x == x) { //縦への移動
res = (Math.abs(y - sel_y) <= 1) ||
((Math.abs(y - sel_y) == 2) && (stage[sel_y+1][sel_x]==__))||
((Math.abs(y - sel_y) == 2) && (stage[sel_y-1][sel_x]==__))||
(CH_SIZE[sel_no][1]==2 && (stage[sel_y+2][sel_x]==__))||
(CH_SIZE[sel_no][1]==2 && (stage[sel_y-2][sel_x]==__));
}
if (sel_y == y) { //横への移動
res = (Math.abs(x - sel_x) <= 1) ||
((Math.abs(x - sel_x) == 2) && (stage[sel_y][sel_x+1]==__))||
((Math.abs(x - sel_x) == 2) && (stage[sel_y][sel_x-1]==__))||
(CH_SIZE[sel_no][0]==2 && stage[sel_y][sel_x+2]==__)||
(CH_SIZE[sel_no][0]==2 && stage[sel_y][sel_x-2]==__);
}
if ((Math.abs(x - sel_x) <= 1) && (Math.abs(y - sel_y) <= 1)) { //斜めへの移動
if ((sel_x +1 == x) && (sel_y -1 == y)) { //右上に移動
res = ((stage[sel_y-1][sel_x]==__)||(stage[sel_y][sel_x+1]==__));
}
if ((sel_x +1 == x) && (sel_y +1 == y)) { //右下に移動
res = ((stage[sel_y+1][sel_x]==__)||(stage[sel_y][sel_x+1]==__));
}
if ((sel_x -1 == x) && (sel_y +1 == y)) { //左下に移動
res = ((stage[sel_y+1][sel_x]==__)||(stage[sel_y][sel_x-1]==__));
}
if ((sel_x -1 == x) && (sel_y -1 == y)) { //左上に移動
res = ((stage[sel_y-1][sel_x]==__)||(stage[sel_y][sel_x-1]==__));
}
}
if ((Math.abs(x - sel_x) >= 3) || (Math.abs(y - sel_y) >= 3) ||
((Math.abs(x - sel_x) == 2) && (Math.abs(y - sel_y) == 1)) ||
((Math.abs(x - sel_x) == 1) && (Math.abs(y - sel_y) == 2)) ||
((CH_SIZE[sel_no][0]==2) && ((Math.abs(x - sel_x) >= 1) && (Math.abs(y - sel_y) >= 1))) ||
((CH_SIZE[sel_no][1]==2) && ((Math.abs(x - sel_x) >= 1) && (Math.abs(y - sel_y) >= 1)))) res = false;
if (!res) return;
console.log("canMove");
// 既存の場所を隙間にする
loop2(sel_h, sel_w, function(e) {
stage[sel_y+e.y][sel_x+e.x] = __;
});
// 新しい位置に駒を移動
loop2(sel_h, sel_w, function(e) {
stage[y+e.y][x+e.x] = sel_no;
});
// 選択継続
sel_x = x; sel_y = y;
turn++;
$("info").innerHTML = "第" + turn + "手 移動しました。";
drawScreen();
// クリア判定
if (sel_no == DT && sel_x == 2 && sel_y == 4) {
setTimeout(gameClear, 1);
}
}
function gameClear() {
document.getElementById('top_title').innerHTML = clear_msg;
$("info").innerHTML = "ゲームクリア! (" + turn + "手)";
drawScreen();
// t_out秒後にゲームを再開する
let count = 0;
const countUp = () => {
$("info").innerHTML = "ゲームクリア! (" + turn + "手) " + (t_out - count) +"";
const timeoutId = setTimeout(countUp,1000);
count++;
if (count > t_out){
clearTimeout(timeoutId);
initGame();
}
}
countUp();
}
// --- 画面の描画 ---
function drawScreen() {
// 箱を描画
ctx.drawImage(images,
CH_POS[XX] * CW, 0, CANVAS_W, CANVAS_H,
0, 0, CANVAS_W, CANVAS_H);
// 隙間と各駒を描画
var tmp = []; // 描画済みを記録するための変数
loop2(ROWS,COLS, function(e) {
row = e.y;
col = e.x;
var no = stage[row] [col];
// 枠あるいは既に描画済みならスキップ
if (tmp[row*COLS+col]||no==XX) return;
var w = CH_SIZE[no][0];
var h = CH_SIZE[no][1];
var p = CH_POS[no];
var tx = col * CW; // 描画先x
var ty = row * CW; // 描画先y
var tw = w * CW; // 描画サイズ幅
var th = h * CW; // 描画高さ
ctx.drawImage (images ,
p * CW, 0, tw, th,
tx, ty, tw, th
);
// 描画済みであることをセット
loop2(h, w, function(f) {
var nx = f.x + col;
var ny = f.y + row;
tmp[ny*COLS+nx] = no;
});
//ゲームクリアなら
if (sel_no == DT && sel_x == 2 && sel_y == 4) {
// ハイライト不要
}else{ //ゲームクリアしてないなら
// 選択範囲をハイライ卜
if (sel_no == no) {
ctx.fillStyle = "rgba(255,100,100,0.5)";
ctx.fillRect(tx, ty, tw, th);
}
}
});
}
// --- 便利関数 ---
// 配列のク口ーンを作成
function cloneArray(a) {
var b = [];
for (var i = 0; i < a.length; i++) {
if (typeof(a[i]) == "object") {
b[i] = cloneArray(a[i]);
} else {
b[i] = a[i];
}
}
return b;
}
// 2重のforルーブをラップした関数
function loop2(rows, cols, callback) {
var e = {x:0, y:0, stop:false};
for (var y = 0; y < rows; y++) {
e.y = y;
for (var x = 0; x < cols; x++) {
e.x = x;
callback(e);
if (e.stop) return false;
}
}
return true;
}
// DOM要素を返す
function $(id) {
return document.getElementById(id);
}
解答例
以下は解答の一例。今回の改修で手数が減りました。