【1日1デプロイ】必ず解ける荷造りパズル「ぴたりトランク便」を作った


今回作ったもの
「1日1デプロイ」の新作として、ブラウザで遊べる荷造りパズル ぴたりトランク便 を作りました。
形の違う荷物を回転させ、トランクの色付きマスへ、はみ出しも重なりもなく詰めます。行き先は海辺の町、高原ホテル、夜市の広場の3か所。旅が進むほど荷物の種類と形が増えていきます。
今回は急いで操作するゲームではありません。制限時間もゲームオーバーもなく、形を眺め、向きを試し、最後のすき間へ荷物が「ぴたり」と収まる気持ちよさを中心にしました。
案内役のブラピさんは、荷物カートに乗ったポーターです。
遊び方
- トレイから荷物をひとつ選ぶ
- 「回す」で90度ずつ向きを変える
- トランクの空いている場所を押して荷物を置く
- すべての色付きマスを埋めると旅支度完了
- 3つの行き先を、できるだけ少ない手数で完成させる
PCではドラッグでも置けます。キーボードでは Tab で荷物を選び、R で回転、矢印キーで置き場所を移動し、Enter で決定できます。
置いた順番は履歴に残るため、「ひとつ戻す」で直前の荷物をトレイへ戻せます。各行き先で1回だけ使える「おまかせ梱包」は、今置ける荷物をひとつ正しい場所へ収めます。
ベスト得点、最少手数、最高到達便、プレイ回数は、この端末のブラウザへ保存します。
今日のレベルアップ1:完成形から作って、必ず解ける問題にする
パズルをランダムに作ると、見た目はそれらしくても「そもそも解けない問題」が混ざることがあります。
今回は、最初に全荷物の正しい位置と向きを決めました。その完成配置が埋めるマスを、トランクの出題範囲にしています。
target = new Set();
level.pieces.forEach(source => {
const solved = {
shape: source.shape,
rotation: source.solution.rot,
x: source.solution.x,
y: source.solution.y
};
cellsFor(solved).forEach(([x, y]) => {
target.add(`${x},${y}`);
});
});
毎日の変化は、正解そのものではなく、トレイに並ぶ順番と最初の向きへ加えます。つまり、見た目や解き始めは変わっても、元になった完成形は失われません。
導入前は、ランダム配置の結果を一つずつ検証する必要がありました。導入後は 完成させてから崩す 順番にしたことで、必ず解ける根拠をデータとして持てるようになりました。
今日のレベルアップ2:回転した形を同じ座標へそろえる
荷物は小さなマスの集まりです。90度回転させると座標にマイナスが出るため、そのままでは置く基準がずれます。
そこで、回転後の左端と上端を求め、必ず (0, 0) から始まる形へそろえました。
function rotateShape(shape, amount) {
let cells = shape.map(([x, y]) => [x, y]);
for (let i = 0; i < ((amount % 4) + 4) % 4; i++) {
cells = cells.map(([x, y]) => [y, -x]);
}
const minX = Math.min(...cells.map(cell => cell[0]));
const minY = Math.min(...cells.map(cell => cell[1]));
return cells.map(([x, y]) => [x - minX, y - minY]);
}
置くときは、回転後の全マスについて次の3点を確認します。
- 6×6のトランクからはみ出していない
- 色の付いた出題範囲に入っている
- すでに置いた荷物と重なっていない
判定を canPlace に集めたので、タップ、ドラッグ、キーボード、おまかせ梱包、撮影用入力のどれでも同じルールを通ります。
今日のデザイン:旅行ポスターと人形のような旅支度
見た目は 古い旅行ポスター × ドールハウスの旅支度 がテーマです。
クリーム、テラコッタ、マスタード、青緑、オリーブ、プラムを使い、荷物には厚紙のような縁と少し丸い押し跡を付けました。行き先が変わると、海辺、高原、夜市の背景へ切り替わります。
正しく置けた荷物は、光の粒と短い上昇音で「ぴたり」と返します。最後の荷物が収まると、トランクのベルトが閉まり、和音とともに旅支度が完成します。
色だけに頼らず、荷物の形、空きマス、選択枠、文章による理由を併用しました。置けないときも減点だけで終わらず、「はみ出し」「範囲外」「重なり」のどれかを短く表示します。
非エンジニア初心者がAIでパズルを作るTIPS
AIへ「ブロックを詰めるパズルを作って」と頼むだけでは、解けない問題や、日によって正解がなくなる問題が生まれやすくなります。
最初の依頼で、ランダムにしてよい部分と、必ず守る部分を分けるのがポイントです。
今回なら、次のように依頼できます。
HTML・CSS・JavaScriptだけで、6×6のトランクへ複数の荷物を詰めるパズルを作ってください。荷物はマス座標の配列で持ち、90度ずつ回転できます。問題はランダムな空きマスから作らず、先に全荷物の完成配置を決め、その完成形が埋めるマスを出題範囲にしてください。日ごとの変化は荷物の順番と初期回転だけにして、必ず解ける状態を保ってください。配置時は、盤外・出題範囲外・既存荷物との重なりを別々に判定し、失敗理由を画面へ表示してください。タップ、ドラッグ、キーボードの入力は同じ配置関数へ集約してください。
作ったあとは「全部置けたか」だけでなく、境界とやり直しを確認します。
左上・右下の端へ置いたときにはみ出し判定が正しいか、4回回すと元の形へ戻るか、重なった荷物を置けないか、失敗後も同じ荷物を選び直せるか、戻す操作で占有マスが空くか、おまかせ梱包がすでに埋まった場所を使わないかを確認してください。3つの行き先を最後まで解き、完成時の得点と結果画面の得点が一致することも確認してください。
「必ず解けるように」だけでなく、なぜ解けるのかを作り方で指定する と、AIと確認しやすくなります。
つまずいたところと直し方
透明な完成演出が、次の操作を止めていた
トランクのベルトを閉じる演出は透明な要素を盤面へ重ねています。見た目が透明でも、要素自体はポインター入力を受け取るため、次の荷物を押せない状態になりました。
演出用の要素へ pointer-events: none を指定し、見た目だけを重ね、操作は下の盤面へ届くようにしました。
完成ボーナス後の表示だけ、古い得点だった
最後の荷物を置いた直後に完成ボーナスを加えていましたが、画面上部の得点を更新する処理がその前で終わっていました。そのため、結果画面は正しいのに、上部だけ古い数字が残りました。
完成ボーナスを加えた直後にHUDを更新し、プレイ中の表示と結果を一致させました。状態を変えたら、内部の数値だけでなく「どの表示を再描画するか」まで確認する必要があります。
撮影用でも、同じ配置処理を使う
縦動画用URLでは、最後の行き先を4個の荷物が入った状態から始め、残り4個を順番に選択・回転・配置します。ただし完成状態を直接作るのではなく、通常プレイと同じ selectPiece、rotateSelected、placeSelected を通しています。
そのため、動画に映る手数、得点、光の粒、ベルト、結果は、実際に遊ぶときと同じ判定から生まれます。
公開URLとまとめ
▶ ぴたりトランク便
今回は、完成形から問題を作る方法、回転後の座標正規化、占有マップによる配置判定、戻す・ヒントの履歴、複数入力を同じ処理へ集約する設計を組み合わせました。
最後のすき間へ荷物が収まり、トランクのベルトが閉まる瞬間まで、考える時間と「ぴたり」の手応えを一つの画面につなげられました。
毎日ひとつ、小さなゲームや便利ツールを公開しています。
Discussion