実験#96:重力シミュレーターゲーム×落ちゲーパズル(物理素材マッチパズル)

■96個目 「物理素材マッチパズル」

こんにちは、AIと作る100のゲーム実験室の みー です。(*^_^*)/

今回は、さわやかな窓辺で楽しむパズルゲームを作ってみました。

テーマはなんと “物理学の落下実験” です。

自動で動くクレーンから素材が落ちてくるシンプルなパズルなのですが、
今回のこだわりは 素材ごとの「落ち方の違い」

「重たい鉄の玉と、軽い鳥の羽。落ちるスピードや揺れ方が違ったら、
パズルはどう変わるんだろう?」

そんな遊び心から生まれた作品になっています。

落ちてくるのは、個性豊かな5つの素材たち。

  • 鉄の玉:ズンッと真っ直ぐ落ちる
  • 鳥の羽:ふわふわ揺れながら落ちる
  • リンゴ:軽やかで安定した落下
  • :バサバサとぶれますが、やや安定した落下
  • 帽子:空気を受けながら揺れ動く落ち方

鉄の玉はストン!と一瞬で着地するのに対して、
鳥の羽は空気の抵抗を受けて右へ左へゆらゆら……。

それぞれの素材のクセを見極めながら、
同じ素材を3つ繋げてポンッとはじけさせていきます。

スコアが上がるにつれて落下スピードも上がり、
のんびりした実験室がだんだんハラハラする展開に。

どんな大連鎖が飛び出すのか、ぜひ楽しんでみてください!

■ AIとのやりとりで面白かった部分

● ゲーム仕様の相談からスタート

みー:(^v^) 今日もゲームを作ります。HTMLで動く落ちゲーで、

落下のリアリティを取り入れたいです。

同じ素材を3つそろえるとはじけて消えるタイプ。
素材は4ブロックで1つの形になっていて、
天井まで積み上がるとゲームオーバー。

前に作ったゲームのコードを貼るので、
それをベースに落ちゲーにしたいです。

〜前回のコードの貼り付け〜

AI:常連様には、毎回ひとつ裏話を。
前回の『驚異の年』の落下実験ゲームも好評でしたね。

さて今回は「落ちゲー」。
落下といえば、ガリレオのピサの斜塔実験が有名ですが、
実はその前にシモン・ステヴィンという学者が
「重さが違っても同時に落ちる」ことを証明していたんです。

歴史の影には先駆者がいるものです。

ご要望の「素材ごとの落下のリアリティ」と
「3つ揃えて消す」要素を組み合わせた新しい落ちゲーを組み上げました。

みー:(つ_<`) これだと普通の落ちゲーになってるし、
4ブロックがバラバラに素材判定されてすぐ消えちゃう。

4つで1つの素材。落ち方も前のゲームみたいにリアルに。
上で左右に動いてクリックで落下、矢印キーは不要です。

AI: ……これは私としたことが。
夜風に気を取られていたのかもしれません。

確かにただのパズルになっていましたね。
4ブロックが独立判定されるのも無粋でした。

「4つで1つの素材」「3手分そろって消える」
本来の美しい連鎖に修正しました。

クレーンの左右移動とクリック落下も再構築済みです。

お詫びにもうひとつ裏話を。
ドイツの「ZARMドロップタワー」では、
146mの塔を真空にしてカプセルを落とし、
地上で4.7秒の無重力を作り出しています。

宇宙に行かずに宇宙を再現する……物理学の執念ですね。

みー:(*=v=) いい感じ。
右クリックで素材を回転できるようにしよう。
あと横幅を1マス増やせる?

AI: 回転が加わると一気に奥深くなりますね。
空中で姿勢を変えるといえば「猫の宙返り問題」。

19世紀に連続写真で撮影され、
猫が体をひねって角運動量保存則を利用していることが判明しました。
無意識に物理を使いこなす猫、恐るべしです。

みー:(^_^;) 他の素材に引っかかって空中で止まるのを直せる?

AI: L字やT字が端に引っかかって浮く現象ですね。

着地時に“固い物体”として固定されていたのが原因でした。

結合を一度解き、
下に空間がある部分は重力で落ちるよう処理を組み替えました。

落下といえば、アポロ15号でのハンマーと羽の実験。

真空では重さに関係なく
同時に落ちることが月面で美しく証明されました。

みー:(^_^)/ 天井に達しても、
同じ素材3つ揃ってたら消えてセーフにしよう。

AI: その仕様、粋ですね。
致死ラインを越えても連鎖が起きれば助かる……
あの逆転劇が生まれます。

パズルとしての快感が一段上がります。

みー:(>v<) 背景が暗いので、さわやかな実験室の窓辺風にしたい。

観葉植物とか置いて、素敵な感じで。

AI: 外は暗くても、画面の中は明るく爽やかに。
窓辺といえば、雨粒の形の話をひとつ。

実は落下中の雨粒は涙型ではなく、
空気抵抗で「おまんじゅう」のように潰れた形になるんです。

そして限界まで潰れると空中で割れて小さな球体に戻る。
私たちが見ていないところで、
空からは無数の“バンズ”が降っているわけです。

■ 実際の動作

まずはこちら。
AIと一緒に作った最初の落下実験ゲームです。

上部を左右に動く実験機から、クリックで素材を落とし、
地面の的にどれだけ近づけるかでポイントが決まります。

(画像:落下実験ゲーム)



このゲームでこだわったのが、素材ごとに異なる落下のクセ

鉄球・羽・リンゴ・本・帽子の5種類があり、
それぞれ重さや空気抵抗の違いで落ち方が変わります。

「どこに着地するか」を予測しながら楽しむ、
ちょっと理科実験のようなゲームです。

(画像:落下実験ゲーム)


そしてこちらが、今回仕上がったパズル版のスタート画面。
背景は明るい窓辺の実験室になり、雰囲気もガラッと変わりました。

(画像:仕上がりのスタート画面)


素材は4つのブロックで1つの形になっていて、
前回のゲームと同じようにリアルな落下をします。

同じ素材を“3つ分”そろえるとポンッと消える仕組みで、
マウスクリックだけで遊べるシンプルなパズルゲームになりました。

(画像:仕上がりのゲーム画面)


■ コードのポイント

今回の記事では、
AI と相談しながら作った「物理素材マッチパズル」 の HTML コードを公開します。

このコードをそのまま保存するだけで、
あなたの PC でもすぐに遊べます。

ブラウザだけで動く、とてもシンプルなゲームなので、
「コードは初めて…」という方でも気軽に試してみてくださいね。

▼ 遊ぶまでの手順(むずかしさ:やさしい)
① テキストエディタを開く

Windows の場合は「メモ帳」、
Mac の場合は「テキストエディット」を使います。
(※検索バーで「メモ帳」と入力するとすぐ出てきます)

② 下のコードをコピーして貼り付けます。
③ 「rakkajikken.html」など、好きな名前で保存します。

(※拡張子は .html にしてください)

④ 保存したファイルをダブルクリックすると、
ブラウザでゲームが起動します。

■ 操作方法

操作はとっても簡単! マウスだけで遊べます。(^^*)

  • 左クリック:素材を「投下」します
  • 右クリック:素材をクルッと「回転」させます

💡 基本のルール

  1. 画面上のクレーンは、自動で左右にゆっくり動いています。
    「ここだ!」と思ったタイミングで左クリックして素材を落としましょう。(^_^)
  2. 落ちてくる素材は、4つのブロックがくっついた“ひとつの塊”。
    この 同じ素材の塊が3つ以上つながる と、ポンッとはじけて消えます。
  3. 素材が着地したあと、下に隙間があればスルスルと落ちていくので、
    思わぬ連鎖が起きることも……!(=v=*)

🚨 実験を成功させるポイント!

  • 落下地点を予測しよう!
    素材によって落ち方が全然違います。
    羽はふわふわ揺れて流されやすく、鉄は一直線にズドン!
    空気抵抗を読むのが勝利のカギ。(´▽`*)
  • 天井突破でも諦めない!
    一番上の枠を超えても、
    すぐに連鎖が起きて枠内に戻れば ギリギリセーフ
    大逆転のチャンスが生まれます。
  • 徐々にスピードアップ!
    スコアが300点上がるごとに、
    クレーンの動きも落下速度もどんどん加速。
    実験室のBGMも少しずつ焦るテンポに変わっていきます……!

ぜひ、さわやかな窓辺で、
ちょっとハラハラする落下実験を楽しんでみてくださいね。(^^*)


“コードは長いので折りたたんでいます。必要な方は開いてご覧ください。

▼ここからコード▼(クリックで開く)
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>落下地点予測パズル - 驚異の年 -</title>
<style>
body {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
background-color: #1a1a1a;
font-family: 'Times New Roman', serif;
user-select: none;
}
canvas {
box-shadow: 0 10px 30px rgba(0,0,0,0.8);
cursor: pointer;
border: 4px solid #fdfbf7;
border-radius: 5px;
}
</style>
</head>
<body>
<canvas id="gameCanvas" width="600" height="800"></canvas>
<script>
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const materials = [
{ name: "鉄の玉", emoji: "🌑", dragX: 0.005, dragY: 0.005, flutter: 0, shape: [[1,1],[1,1]] },
{ name: "鳥の羽", emoji: "🪶", dragX: 0.05, dragY: 0.15, flutter: 0.15, shape: [[1],[1],[1],[1]] },
{ name: "リンゴ", emoji: "🍎", dragX: 0.01, dragY: 0.02, flutter: 0.01, shape: [[0,1,0],[1,1,1]] },
{ name: "本", emoji: "📖", dragX: 0.03, dragY: 0.05, flutter: 0.05, shape: [[1,0],[1,0],[1,1]] },
{ name: "帽子", emoji: "🤠", dragX: 0.04, dragY: 0.1, flutter: 0.08, shape: [[0,1,1],[1,1,0]] }
];
const COLS = 9;
const VISIBLE_ROWS = 13;
const HIDDEN_ROWS = 4;
const ROWS = VISIBLE_ROWS + HIDDEN_ROWS;
const CELL_SIZE = 40;
const BOARD_X = 120;
const BOARD_Y = 220;
let board = Array.from({ length: ROWS }, () => Array(COLS).fill(null));
let state = 'start';
let score = 0;
let globalPieceId = 0;
let speedFactor = 1.0; // ゲーム全体のスピード倍率
let craneX = 300;
let prevCraneX = 300;
let craneVx = 0;
let currentPiece = null;
let objX = 0, objY = 0;
let objVx = 0, objVy = 0;
const baseGravity = 0.2;
let audioCtx;
let nextNoteTime = 0;
let noteIndex = 0;
const bgmNotes = [349.23, 392.00, 440.00, 392.00, 349.23, 329.63, 293.66, 329.63, 349.23, 440.00, 392.00, 392.00];
function initAudio() {
if (!audioCtx) {
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
nextNoteTime = audioCtx.currentTime + 0.1;
}
}
function playBGM() {
if (!audioCtx || state === 'start' || state === 'gameover') return;
// スピード倍率に応じてBGMのテンポも速くする(限界値は0.15秒)
let currentTempo = Math.max(0.15, 0.4 / speedFactor);
while (nextNoteTime < audioCtx.currentTime + 0.1) {
let freq = bgmNotes[noteIndex % bgmNotes.length] * 1.5;
let osc = audioCtx.createOscillator();
let gain = audioCtx.createGain();
osc.type = 'sine';
osc.frequency.value = freq;
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start(nextNoteTime);
gain.gain.setValueAtTime(0.03, nextNoteTime);
gain.gain.exponentialRampToValueAtTime(0.001, nextNoteTime + currentTempo - 0.05);
osc.stop(nextNoteTime + currentTempo);
nextNoteTime += currentTempo;
noteIndex++;
}
}
function playDropSE() {
if(!audioCtx) return;
let osc = audioCtx.createOscillator();
let gain = audioCtx.createGain();
osc.type = 'square';
osc.frequency.setValueAtTime(600, audioCtx.currentTime);
osc.frequency.exponentialRampToValueAtTime(100, audioCtx.currentTime + 0.3);
osc.connect(gain);
gain.connect(audioCtx.destination);
gain.gain.setValueAtTime(0.03, audioCtx.currentTime);
gain.gain.linearRampToValueAtTime(0, audioCtx.currentTime + 0.3);
osc.start();
osc.stop(audioCtx.currentTime + 0.3);
}
function playHitSE() {
if(!audioCtx) return;
let osc = audioCtx.createOscillator();
let gain = audioCtx.createGain();
osc.type = 'triangle';
osc.frequency.setValueAtTime(1000, audioCtx.currentTime);
osc.frequency.setValueAtTime(1500, audioCtx.currentTime + 0.1);
osc.connect(gain);
gain.connect(audioCtx.destination);
gain.gain.setValueAtTime(0.08, audioCtx.currentTime);
gain.gain.linearRampToValueAtTime(0, audioCtx.currentTime + 0.2);
osc.start();
osc.stop(audioCtx.currentTime + 0.2);
}
function playRotateSE() {
if(!audioCtx) return;
let osc = audioCtx.createOscillator();
let gain = audioCtx.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(1200, audioCtx.currentTime);
osc.frequency.exponentialRampToValueAtTime(1800, audioCtx.currentTime + 0.05);
osc.connect(gain);
gain.connect(audioCtx.destination);
gain.gain.setValueAtTime(0.03, audioCtx.currentTime);
gain.gain.linearRampToValueAtTime(0, audioCtx.currentTime + 0.05);
osc.start();
osc.stop(audioCtx.currentTime + 0.05);
}
function setupNextPiece() {
let baseMat = materials[Math.floor(Math.random() * materials.length)];
currentPiece = {
mat: baseMat,
shape: baseMat.shape.map(row => [...row]),
id: ++globalPieceId
};
}
function rotateShape(matrix) {
return matrix[0].map((val, index) => matrix.map(row => row[index]).reverse());
}
function checkCollision(shape, c, r) {
for (let sr = 0; sr < shape.length; sr++) {
for (let sc = 0; sc < shape[sr].length; sc++) {
if (shape[sr][sc]) {
let boardR = r + sr;
let boardC = c + sc;
if (boardR >= ROWS) return true;
if (boardC < 0 || boardC >= COLS) return true;
if (boardR >= 0 && board[boardR][boardC] !== null) return true;
}
}
}
return false;
}
function getDropRow(shape, col) {
for (let r = -shape.length; r < ROWS; r++) {
if (checkCollision(shape, col, r + 1)) return r;
}
return ROWS - shape.length;
}
function lockPiece(col, row) {
for (let sr = 0; sr < currentPiece.shape.length; sr++) {
for (let sc = 0; sc < currentPiece.shape[sr].length; sc++) {
if (currentPiece.shape[sr][sc]) {
let boardR = row + sr;
let boardC = col + sc;
if (boardR >= 0 && boardR < ROWS) {
board[boardR][boardC] = { mat: currentPiece.mat, id: currentPiece.id };
}
}
}
}
state = 'gravityWait';
gravityTimer = Date.now();
}
function applyGravity() {
let moved = false;
for (let c = 0; c < COLS; c++) {
for (let r = ROWS - 2; r >= 0; r--) {
if (board[r][c] !== null && board[r + 1][c] === null) {
board[r + 1][c] = board[r][c];
board[r][c] = null;
moved = true;
}
}
}
return moved;
}
function findMatches() {
let visited = Array.from({ length: ROWS }, () => Array(COLS).fill(false));
let toDelete = [];
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS; c++) {
if (board[r][c] !== null && !visited[r][c]) {
let matName = board[r][c].mat.name;
let clusterCells = [];
let uniqueIds = new Set();
let stack = [[r, c]];
while (stack.length > 0) {
let [currR, currC] = stack.pop();
if (currR < 0 || currR >= ROWS || currC < 0 || currC >= COLS) continue;
if (visited[currR][currC] || board[currR][currC] === null || board[currR][currC].mat.name !== matName) continue;
visited[currR][currC] = true;
clusterCells.push([currR, currC]);
uniqueIds.add(board[currR][currC].id);
stack.push([currR - 1, currC], [currR + 1, currC], [currR, currC - 1], [currR, currC + 1]);
}
if (uniqueIds.size >= 3) toDelete.push(...clusterCells);
}
}
}
return toDelete;
}
let matchTimer = 0;
let gravityTimer = 0;
canvas.addEventListener('contextmenu', e => e.preventDefault());
canvas.addEventListener('mousedown', (e) => {
if (e.button === 2) {
if (state === 'falling') {
let rotated = rotateShape(currentPiece.shape);
let maxCol = Math.max(0, COLS - rotated[0].length);
let col = Math.round((objX - BOARD_X) / CELL_SIZE);
col = Math.max(0, Math.min(maxCol, col));
let dropRow = getDropRow(rotated, col);
let dropY = BOARD_Y + (dropRow - HIDDEN_ROWS) * CELL_SIZE;
if (objY <= dropY + 15) {
currentPiece.shape = rotated;
objX = BOARD_X + col * CELL_SIZE;
playRotateSE();
}
} else if (state === 'aiming') {
currentPiece.shape = rotateShape(currentPiece.shape);
playRotateSE();
}
return;
}
if (state === 'start') {
initAudio();
board = Array.from({ length: ROWS }, () => Array(COLS).fill(null));
score = 0;
speedFactor = 1.0;
globalPieceId = 0;
setupNextPiece();
state = 'aiming';
} else if (state === 'aiming') {
playDropSE();
objX = craneX - (currentPiece.shape[0].length * CELL_SIZE) / 2;
objY = 160;
// クレーンの速度も上がっているため、投下時の横慣性も上がる
objVx = craneVx * 1.2;
objVy = 0;
state = 'falling';
} else if (state === 'gameover') {
state = 'start';
}
});
function update() {
playBGM();
let time = Date.now() / 1000;
// 300点ごとにスピードが15%ずつアップしていく
speedFactor = 1.0 + Math.floor(score / 300) * 0.15;
prevCraneX = craneX;
// クレーンの移動速度もスピード倍率に合わせる
craneX = 300 + Math.sin(time * 1.5 * speedFactor) * 120;
craneVx = (craneX - prevCraneX);
if (state === 'falling') {
// 重力もスピード倍率で強化
objVy += baseGravity * speedFactor;
objVx *= (1 - currentPiece.mat.dragX);
objVy *= (1 - currentPiece.mat.dragY);
objVx += Math.sin(objY / 30) * currentPiece.mat.flutter;
let nextX = objX + objVx;
let maxCol = Math.max(0, COLS - currentPiece.shape[0].length);
let minX = BOARD_X;
let maxX = BOARD_X + maxCol * CELL_SIZE;
if (nextX < minX) { nextX = minX; objVx *= -0.5; }
if (nextX > maxX) { nextX = maxX; objVx *= -0.5; }
let intendedCol = Math.round((nextX - BOARD_X) / CELL_SIZE);
intendedCol = Math.max(0, Math.min(maxCol, intendedCol));
let dropRowIntended = getDropRow(currentPiece.shape, intendedCol);
let dropYIntended = BOARD_Y + (dropRowIntended - HIDDEN_ROWS) * CELL_SIZE;
if (objY > dropYIntended + 15) {
objVx *= -0.5;
} else {
objX = nextX;
}
objY += objVy;
let col = Math.round((objX - BOARD_X) / CELL_SIZE);
let dropRow = getDropRow(currentPiece.shape, col);
let dropY = BOARD_Y + (dropRow - HIDDEN_ROWS) * CELL_SIZE;
if (objY >= dropY) {
objY = dropY;
lockPiece(col, dropRow);
}
} else if (state === 'matching') {
// スピード倍率が高いほど、判定の待ち時間が短くなる
if (Date.now() - matchTimer > 300 / speedFactor) {
let matches = findMatches();
if (matches.length > 0) {
matches.forEach(([r, c]) => { board[r][c] = null; });
score += matches.length * 10;
playHitSE();
state = 'gravityWait';
gravityTimer = Date.now();
} else {
let isGameOver = false;
for (let c = 0; c < COLS; c++) {
for (let r = 0; r < HIDDEN_ROWS; r++) {
if (board[r][c] !== null) isGameOver = true;
}
}
if (isGameOver) {
state = 'gameover';
} else {
setupNextPiece();
state = 'aiming';
}
}
}
} else if (state === 'gravityWait') {
// パラパラ落ちる速度もアップ
if (Date.now() - gravityTimer > 100 / speedFactor) {
if (applyGravity()) {
gravityTimer = Date.now();
} else {
state = 'matching';
matchTimer = Date.now();
}
}
}
}
function drawBackground() {
let skyGradient = ctx.createLinearGradient(0, 0, 0, 600);
skyGradient.addColorStop(0, '#87CEEB');
skyGradient.addColorStop(1, '#E0F6FF');
ctx.fillStyle = skyGradient;
ctx.fillRect(0, 0, 600, 800);
ctx.fillStyle = 'rgba(255, 255, 255, 0.7)';
ctx.beginPath();
ctx.arc(120, 250, 60, 0, Math.PI * 2);
ctx.arc(180, 230, 80, 0, Math.PI * 2);
ctx.arc(240, 260, 50, 0, Math.PI * 2);
ctx.arc(450, 150, 50, 0, Math.PI * 2);
ctx.arc(500, 140, 70, 0, Math.PI * 2);
ctx.arc(550, 160, 40, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#FAFAFA';
ctx.fillRect(0, 0, 600, 20);
ctx.fillRect(0, 0, 20, 800);
ctx.fillRect(580, 0, 20, 800);
ctx.fillRect(290, 0, 20, 800);
ctx.fillRect(0, 450, 600, 20);
ctx.fillStyle = 'rgba(0,0,0,0.1)';
ctx.fillRect(20, 20, 10, 780);
ctx.fillRect(310, 20, 10, 780);
ctx.fillRect(20, 470, 560, 10);
let desk = ctx.createLinearGradient(0, 720, 0, 800);
desk.addColorStop(0, '#D4A373');
desk.addColorStop(1, '#A06E3B');
ctx.fillStyle = desk;
ctx.fillRect(0, 720, 600, 80);
ctx.fillStyle = '#8B5A2B';
ctx.fillRect(0, 720, 600, 10);
ctx.font = '45px sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'bottom';
ctx.fillText('🪴', 60, 725);
ctx.fillText('🧪', 160, 725);
ctx.fillText('🔬', 440, 725);
ctx.fillText('🌿', 540, 725);
ctx.textBaseline = 'top';
ctx.fillText('🌿', 45, 20);
ctx.fillText('🌿', 555, 20);
}
// 角丸の四角形を描くための補助関数
function drawRoundRect(x, y, w, h, r) {
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.lineTo(x + w - r, y);
ctx.quadraticCurveTo(x + w, y, x + w, y + r);
ctx.lineTo(x + w, y + h - r);
ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
ctx.lineTo(x + r, y + h);
ctx.quadraticCurveTo(x, y + h, x, y + h - r);
ctx.lineTo(x, y + r);
ctx.quadraticCurveTo(x, y, x + r, y);
ctx.closePath();
ctx.fill();
ctx.stroke();
}
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawBackground();
ctx.fillStyle = '#BDC3C7';
ctx.fillRect(0, 80, 600, 15);
ctx.fillStyle = '#7F8C8D';
ctx.fillRect(0, 95, 600, 5);
ctx.fillStyle = '#34495E';
ctx.fillRect(craneX - 25, 70, 50, 40);
ctx.beginPath();
ctx.moveTo(craneX, 110);
ctx.lineTo(craneX, (state === 'aiming') ? 160 : 100);
ctx.lineWidth = 3;
ctx.strokeStyle = '#95A5A6';
ctx.stroke();
ctx.fillStyle = 'rgba(255, 255, 255, 0.3)';
ctx.fillRect(BOARD_X, BOARD_Y, COLS * CELL_SIZE, VISIBLE_ROWS * CELL_SIZE);
ctx.strokeStyle = 'rgba(255, 255, 255, 0.8)';
ctx.lineWidth = 4;
ctx.strokeRect(BOARD_X, BOARD_Y, COLS * CELL_SIZE, VISIBLE_ROWS * CELL_SIZE);
ctx.strokeStyle = 'rgba(255, 255, 255, 0.4)';
ctx.lineWidth = 1;
for(let i=1; i<COLS; i++) {
ctx.beginPath(); ctx.moveTo(BOARD_X + i*CELL_SIZE, BOARD_Y); ctx.lineTo(BOARD_X + i*CELL_SIZE, BOARD_Y + VISIBLE_ROWS*CELL_SIZE); ctx.stroke();
}
for(let i=1; i<VISIBLE_ROWS; i++) {
ctx.beginPath(); ctx.moveTo(BOARD_X, BOARD_Y + i*CELL_SIZE); ctx.lineTo(BOARD_X + COLS*CELL_SIZE, BOARD_Y + i*CELL_SIZE); ctx.stroke();
}
const drawBlock = (x, y, emoji, alpha = 1) => {
ctx.globalAlpha = alpha;
// ブロックの視認性を高めるための白いタイル背景
ctx.fillStyle = '#F8F9FA';
ctx.strokeStyle = '#E5E7E9';
ctx.lineWidth = 2;
// 少し浮き出たような影をつける
ctx.shadowColor = 'rgba(0, 0, 0, 0.2)';
ctx.shadowBlur = 3;
ctx.shadowOffsetY = 2;
drawRoundRect(x + 2, y + 2, CELL_SIZE - 4, CELL_SIZE - 4, 6);
// 影をリセットして絵文字をクッキリ描画
ctx.shadowColor = 'transparent';
ctx.font = '28px sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(emoji, x + CELL_SIZE / 2, y + CELL_SIZE / 2 + 2);
ctx.globalAlpha = 1.0;
};
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS; c++) {
if (board[r][c]) {
drawBlock(BOARD_X + c * CELL_SIZE, BOARD_Y + (r - HIDDEN_ROWS) * CELL_SIZE, board[r][c].mat.emoji);
}
}
}
if (state === 'aiming' || state === 'falling') {
let px = (state === 'aiming') ? craneX - (currentPiece.shape[0].length * CELL_SIZE) / 2 : objX;
let py = (state === 'aiming') ? 160 : objY;
for (let r = 0; r < currentPiece.shape.length; r++) {
for (let c = 0; c < currentPiece.shape[r].length; c++) {
if (currentPiece.shape[r][c]) {
drawBlock(px + c * CELL_SIZE, py + r * CELL_SIZE, currentPiece.mat.emoji);
}
}
}
}
ctx.fillStyle = 'rgba(255, 255, 255, 0.85)';
ctx.fillRect(10, 10, 580, 40);
ctx.fillStyle = '#2C3E50';
ctx.font = 'bold 20px "Times New Roman", serif';
ctx.textAlign = 'left';
ctx.textBaseline = 'middle';
// 倍率も表示してスピードアップ感を演出
let speedDisplay = speedFactor > 1.0 ? ` (Speed x${speedFactor.toFixed(2)})` : '';
ctx.fillText('スコア: ' + score + speedDisplay, 20, 30);
if (currentPiece) {
ctx.textAlign = 'right';
ctx.fillText('次期実験物質: ' + currentPiece.mat.name, 570, 30);
}
if (state === 'start') {
ctx.fillStyle = 'rgba(255, 255, 255, 0.7)';
ctx.fillRect(0, 0, 600, 800);
ctx.fillStyle = '#FDFBF7';
ctx.fillRect(100, 300, 400, 160);
ctx.strokeStyle = '#2C3E50';
ctx.strokeRect(100, 300, 400, 160);
ctx.fillStyle = '#2980B9';
ctx.font = 'bold 28px "Times New Roman", serif';
ctx.textAlign = 'center';
ctx.fillText('落下パズル - 驚異の窓辺 -', 300, 345);
ctx.fillStyle = '#34495E';
ctx.font = '16px "Times New Roman", serif';
ctx.fillText('左クリックで投下 / 右クリックで回転', 300, 395);
ctx.fillText('同じ物質の「塊」を3つ繋げて消去せよ', 300, 425);
}
if (state === 'gameover') {
ctx.fillStyle = 'rgba(255, 255, 255, 0.7)';
ctx.fillRect(0, 0, 600, 800);
ctx.fillStyle = '#FDFBF7';
ctx.fillRect(100, 300, 400, 150);
ctx.strokeStyle = '#E74C3C';
ctx.strokeRect(100, 300, 400, 150);
ctx.fillStyle = '#E74C3C';
ctx.textAlign = 'center';
ctx.font = 'bold 36px "Times New Roman", serif';
ctx.fillText('限界突破(実験終了)', 300, 360);
ctx.fillStyle = '#2C3E50';
ctx.font = '24px "Times New Roman", serif';
ctx.fillText('最終スコア: ' + score, 300, 410);
}
}
function gameLoop() {
if (state !== 'start' && state !== 'gameover') update();
draw();
requestAnimationFrame(gameLoop);
}
gameLoop();
</script>
</body>
</html>


■ 今日の学び

今回はゲームそのものの制作に加えて、
「AIにどう相談するか」 という点もひとつの実験テーマでした。

「前に作ったゲームのコードを直接貼れば、
言葉で説明するより世界観が伝わるのでは?」

そう思って試してみたのですが、最
初に出てきたのはまさかの “ごく普通の落ちゲー”

コードを渡しただけでは、
私がどこを一番大切にしたいのかまでは伝わらなかったんですね。꜀( ꜆-ࡇ-)꜆

でも、ここからが AI と作る面白さ。
「前作のリアルな落下感を残したい」
「4つの塊で1つの素材として扱いたい」

そんなふうに少しずつ方向を修正していくことで、
前作の世界観をしっかり受け継いだ新しいパズルに仕上がっていきました。

制作の合間には、AIから「落下の歴史」についての
不思議で面白い雑学もたくさん教えてもらいました。

記事には書ききれなかったけれど、
ただ作るだけでなく、
知的好奇心をくすぐられながら進む制作時間はとても贅沢でした。( ੭ ˙ᗜ˙ )੭ ੈ✩

これからも AI との対話のコツを探りながら、
もっといろいろなゲームを生み出していきたいと思います。

■ 次回は、キャラクター作りからのゲームです。

次回は、ゲームづくりの前に キャラクター制作 からスタートします。

今回生まれたのは、薬草茶が大好きな 「茶虫くん」

お茶をすすりながら、のんびり前へ前へと進む茶虫くん。
その前に広がるのは、水たまりだらけの道……。

あなたは、いろいろな薬草の葉っぱを並べて、
茶虫くんが落ちないように“道”を作ってあげる ことになります。

次回も、楽しみにしてくださいね(*^v^)ノシ

▶ 次の実験はこちら  
(絵本のような世界・やさしさと健康)  
「茶虫くんの薬草茶の旅 〜 水たまり大作戦 〜」



 ✨今日も最後まで読んでくださって、ありがとうございます。
みーの実験室は、みなさんの応援で元気に動いています。(*^v^*)

~このブログについて~

AIと一緒に遊び、
100の実験でゲームを育てるブログ

🌈初めての方へ
🌱このブログについて
🌳実験一覧
🍃プライバシーポリシー
🐣未記事ゲーム展示室


AIと作る100のゲーム実験室(*^v^*)をもっと見る

今すぐ購読し、続きを読んで、すべてのアーカイブにアクセスしましょう。

続きを読む