実験 #01:はじめての壁破りゲーム(BREAK BLOCK)

■ 「BREAK BLOCK」記念すべき1作品目!ヽ(^w^*)

 今回は、AIと一緒に作った落ちゲー「BREAK BLOCK」を紹介します。

 まだ、この時点でAIとのやり取りをいったん終わって(ログアウトして)、続きから始めることできるかどうかよくわからなくて。一回のやり取りで済ませようとしてました(つ_=;)

 なので、前回の盆栽ゲームの時は、お休みの日に丸々1日AIとのやりとりを続けてました( ゜o ゜)~*

 そして今回のゲームですが、前回記事のテーマにもなった【壁】を、タイトルの通り、壊すべく作ったのが、「BREAK BLOCK」です。

 はっきり言って、ぷよぷよっぽいゲームです。(・._.・`)
 基本は同じ感じで4つ同じブロックがそろうと砕けて消えます。 

 モードが4つあります。
  NORMAL:基本モード
  HELL:落下速度が速い
  SEE-THRU:途中からブロックの形だけ見える
  EYES ONLY:目だけが見える
 
 最初こうしようと思ってたのが、色が消えて見えなくなるシースルーモードで遊んでみたいと思ったことでした。

 ・・・だけど作ってみると、鬼のような難しさでした。(><;)

 そこで追加で作ったのが、色を消して特徴的な色ごとの目つきを残した。アイモードです。目つきのヒントはあるもののやっぱり難しい仕上がりになってます。(^^;)

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

(^^*)パズルゲームでいわゆる落ちげーを作りたいと思うんだけどできる?

(AI)メモ帳だけで「落ちゲー」を作るのは、少し難易度が上がりますが可能です!

 AIの説明してくれていることはよく分からないのですが、ゲーム自体はもともとのイメージが分かっているので、それに合わせて仕上げていくように相談して作っていきました。(^~^)

 ある程度できたところで、前日の「盆栽ゲーム」疲れもあり、すこし休憩しようとしたところで、ついページを閉じてしまいました。Σ(・.・;)
そして再開。

(:>v<)”間違えてログアウトしちゃったけど、さっきの落ちげー作りのつづきできますか?

(AI)おかえりなさい!ログアウトしちゃうと焦りますよね。でも大丈夫、バッチリ続きから再開できますよ!✨さっきまで作っていた**「落ちゲー」**のコードや設計、覚えています。中断したところから再開しましょう。

(つ_<、)よかったです。1からにならなくて・・・やさしい言葉に安心します。
  分からなかったことが分かる安心感・・・
  研究(学び)の成果は出ている・・・はず。(>v<)”

■ 実際の動作

BREAK BLOCK

(@_@)ぷよではなくブロックです。

シースルーモード

アイモード

AIが出してくれたタイトル画面のイメージ画像

■ コードのポイント

 修正とかとかしていると差し込み用のコードを出してくれるのですが、コードよくわからないので、どこから差し込んだらいいのかもさっぱり。(=‗=`)「

 【(^^;)コードの差し込みは心配なので、差し込み後の全体コピーできる状態のコードを下さい】という感じでよくAIにお願いしてました。

 今回の記事では、AI と相談しながら作った「BREAK BLOCK」の HTML コードを公開します。

 このコードは、ブラウザだけで動くシンプルなものです。
 PCの「メモ帳」を開いて、
 下のコードを **全部コピーして「block.html」などの名前で保存**し、
 ブラウザで開くとそのまま遊べます。
 キーボードの矢印左右キーで移動。下で落下加速。上ボタンで瞬間着地ができます。
 今回もAI が作ってくれたコードをそのまま載せているので、
 自分で改造したり、動きを変えたり、色を変えたりして遊んでみてください。(^^*)

 「※スマホでは動作しない場合があります」

 「※コードが長いので、必要な方だけコピーしてください」

▼ここからコード▼(クリックして開く)
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>BREAK BLOCK - Ultimate Edition</title>
<style>
body {
text-align: center;
background-color: #331100;
background-image:
linear-gradient(335deg, #220a00 23px, transparent 23px),
linear-gradient(155deg, #2c0d00 23px, transparent 23px),
linear-gradient(335deg, #220a00 23px, transparent 23px),
linear-gradient(155deg, #2c0d00 23px, transparent 23px);
background-size: 58px 58px;
background-position: 0px 2px, 4px 35px, 29px 31px, 34px 6px;
color: #fff; font-family: 'Arial Black', sans-serif;
display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100vh; margin: 0; overflow: hidden;
}
#main-wrapper { transition: transform 0.05s; position: relative; }
.title-break { font-size: 80px; margin: 0; line-height: 0.9; color: #fff; text-shadow: 0 5px 0 #aaa, 0 0 20px rgba(255,100,0,0.8); animation: glow 2s infinite alternate; }
.title-block { font-size: 60px; margin: -10px 0 20px 0; color: #ff3366; text-shadow: 3px 3px 0 #500; }
@keyframes glow { from { text-shadow: 0 0 10px #ff6600, 0 5px 0 #aaa; } to { text-shadow: 0 0 25px #ffcc00, 0 5px 0 #aaa; } }
.game-container { display: flex; gap: 30px; align-items: flex-start; position: relative; }
canvas { border: 10px solid #5a2d0c; background: #000; box-shadow: 0 0 60px rgba(0,0,0,0.9); border-radius: 8px; }
#comboText { position: absolute; top: 40%; left: 50%; transform: translate(-50%, -50%); font-size: 50px; font-weight: bold; text-shadow: 4px 4px 0 #000; z-index: 5; pointer-events: none; opacity: 0; text-align: center; line-height: 1.2; }
.combo-anim { animation: combo-pop 0.6s ease-out forwards; }
@keyframes combo-pop { 0% { transform: translate(-50%, -40%) scale(0.5); opacity: 0; } 30% { transform: translate(-50%, -55%) scale(1.2); opacity: 1; } 100% { transform: translate(-50%, -60%) scale(1.1); opacity: 0; } }
.shake { animation: shake-anim 0.1s linear infinite; }
@keyframes shake-anim { 0% { transform: translate(0, 0); } 25% { transform: translate(-8px, 6px); } 50% { transform: translate(8px, -6px); } 100% { transform: translate(0, 0); } }
.side-panel { background: rgba(42, 42, 42, 0.9); padding: 20px; border-radius: 15px; border: 5px solid #5a2d0c; width: 170px; display: flex; flex-direction: column; gap: 15px; }
.info-box { border-bottom: 2px dashed #5a2d0c; padding-bottom: 10px; }
.label { color: #ffcc00; font-size: 14px; margin-bottom: 5px; }
.value { font-size: 24px; color: #fff; }
.best-list { font-size: 12px; text-align: left; color: #ccc; }
.best-item { display: flex; justify-content: space-between; margin: 2px 0; }
.best-score { color: #00ccff; font-weight: bold; }
#overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: rgba(10, 5, 0, 0.95); display: flex; flex-direction: column; align-items: center; justify-content: center; z-index: 20; }
.btn { background: #444; color: #fff; border: 2px solid #666; padding: 15px; font-size: 16px; cursor: pointer; border-radius: 8px; margin: 5px; width: 150px; font-weight: bold; }
.btn:hover { background: #ff3366; border-color: #fff; }
.new-record-msg { color: #ffcc00; font-size: 24px; animation: bounce 0.5s infinite alternate; margin-bottom: 10px; }
@keyframes bounce { from { transform: scale(1); } to { transform: scale(1.1); } }
</style>
</head>
<body>
<div id="main-wrapper">
<div class="game-container">
<canvas id="gameCanvas" width="360" height="720"></canvas>
<div id="comboText"></div>
<div id="overlay">
<div id="overlay-content">
<div class="title-break">BREAK</div><div class="title-block">BLOCK</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
<button class="btn" onclick="startGame('NORMAL')">NORMAL</button>
<button class="btn" onclick="startGame('HELL')" style="color:#ff5500;">🔥 HELL 🔥</button>
<button class="btn" onclick="startGame('SEE-THRU')" style="color:#00ccff;">SEE-THRU</button>
<button class="btn" onclick="startGame('EYES ONLY')" style="color:#ff3366;">EYES ONLY</button>
</div>
</div>
</div>
<div class="side-panel">
<div class="info-box">
<div class="label">NEXT</div>
<canvas id="nextCanvas" width="100" height="100" style="border:none; box-shadow:none;"></canvas>
</div>
<div class="info-box">
<div class="label">SCORE</div>
<div id="scoreValue" class="value">0</div>
</div>
<div class="info-box">
<div class="label">BEST SCORES</div>
<div class="best-list" id="bestDisplay">
<div class="best-item"><span>NORMAL:</span><span class="best-score" id="best-NORMAL">0</span></div>
<div class="best-item"><span>HELL:</span><span class="best-score" id="best-HELL">0</span></div>
<div class="best-item"><span>SEE-THRU:</span><span class="best-score" id="best-SEE-THRU">0</span></div>
<div class="best-item"><span>EYES ONLY:</span><span class="best-score" id="best-EYES ONLY">0</span></div>
</div>
</div>
<div style="font-size:12px; margin-top:5px; color:#aaa;">MODE: <span id="modeValue">-</span></div>
</div>
</div>
</div>
<script>
const wrapper = document.getElementById("main-wrapper"), canvas = document.getElementById("gameCanvas"), ctx = canvas.getContext("2d");
const nextCtx = document.getElementById("nextCanvas").getContext("2d"), comboEl = document.getElementById("comboText");
const ROWS = 12, COLS = 6, SIZE = 60, COLORS = ["#FF3366", "#22FF88", "#22CCFF", "#FFCC00"];
let board, p1, p2, rot, score, gameActive = false, lookDir = 0, gameTimer, currentMode, nextC1, nextC2;
let audioCtx, reachBlocks = [];
// --- ランキング保存ロジック ---
const highScores = JSON.parse(localStorage.getItem('breakBlockBest')) || {
'NORMAL': 0, 'HELL': 0, 'SEE-THRU': 0, 'EYES ONLY': 0
};
function updateBestDisplay() {
for (let m in highScores) {
const el = document.getElementById(`best-${m}`);
if (el) el.innerText = highScores[m];
}
}
updateBestDisplay();
function initAudio() {
if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
if (audioCtx.state === 'suspended') audioCtx.resume();
}
function playSound(f, t, d, v = 0.1) {
if(!audioCtx) return;
try {
const o = audioCtx.createOscillator(), g = audioCtx.createGain();
o.type = t; o.frequency.setValueAtTime(f, audioCtx.currentTime);
o.connect(g); g.connect(audioCtx.destination);
g.gain.setValueAtTime(v, audioCtx.currentTime);
g.gain.exponentialRampToValueAtTime(0.0001, audioCtx.currentTime + d);
o.start(); o.stop(audioCtx.currentTime + d);
} catch(e){}
}
function showCombo(chain) {
const words = ["PA-KIN!", "BAKI!", "GO!", "DOGAN!", "MAX!"];
comboEl.innerText = `${chain} COMBO\n${words[Math.min(chain-1, 4)]}`;
comboEl.style.color = COLORS[chain % 4];
comboEl.classList.remove("combo-anim"); void comboEl.offsetWidth; comboEl.classList.add("combo-anim");
wrapper.classList.add("shake");
setTimeout(() => wrapper.classList.remove("shake"), 100);
}
function updateReachStatus() {
reachBlocks = [];
if (!p1 || !gameActive) return;
let chk = Array.from({length:ROWS},()=>Array(COLS).fill(false));
for(let y=0; y<ROWS; y++) {
for(let x=0; x<COLS; x++) {
if(board[y][x] !== 0 && !chk[y][x]) {
let conn = []; findConn(x, y, board[y][x], conn, chk);
if(conn.length === 3 && (board[y][x] === p1.c || board[y][x] === p2.c)) {
reachBlocks.push(...conn);
}
}
}
}
}
function drawBlock(tCtx, x, y, cCode, s = SIZE, isStatic = false) {
if (!cCode) return;
let isBelowLine = (currentMode === 'SEE-THRU' || currentMode === 'EYES ONLY') && y >= 2;
let colorIdx = COLORS.indexOf(cCode);
// リーチ発光演出
let isReaching = reachBlocks.some(b => b.x === x && b.y === y);
if (isReaching && !isStatic) {
let pulse = (Math.sin(Date.now() / 150) + 1) / 2;
tCtx.save();
tCtx.shadowBlur = 15 + (pulse * 10);
tCtx.shadowColor = "white";
tCtx.strokeStyle = "white";
tCtx.lineWidth = 2 + (pulse * 3);
tCtx.roundRect(x * s + 3, y * s + 3, s - 6, s - 6, 12);
tCtx.stroke();
tCtx.restore();
}
tCtx.fillStyle = isBelowLine ? "#222" : cCode;
tCtx.beginPath(); tCtx.roundRect(x * s + 3, y * s + 3, s - 6, s - 6, 12); tCtx.fill();
let showEyes = (currentMode !== 'SEE-THRU' || !isBelowLine);
if (showEyes) {
let eX = isStatic ? 0 : lookDir * 5, eY = y * s + s * 0.45;
tCtx.fillStyle = "white"; tCtx.beginPath();
tCtx.arc(x * s + s*0.3 + eX, eY, s*0.13, 0, 7); tCtx.arc(x * s + s*0.7 + eX, eY, s*0.13, 0, 7); tCtx.fill();
tCtx.fillStyle = "black"; tCtx.beginPath();
tCtx.arc(x * s + s*0.3 + eX*1.4, eY+1, s*0.06, 0, 7); tCtx.arc(x * s + s*0.7 + eX*1.4, eY+1, s*0.06, 0, 7); tCtx.fill();
tCtx.strokeStyle = "rgba(0,0,0,0.5)"; tCtx.lineWidth = 3;
if (colorIdx === 0) { // 赤
tCtx.beginPath(); tCtx.moveTo(x*s+s*0.15, eY-10); tCtx.lineTo(x*s+s*0.4, eY-3); tCtx.moveTo(x*s+s*0.85, eY-10); tCtx.lineTo(x*s+s*0.6, eY-3); tCtx.stroke();
} else if (colorIdx === 1) { // 緑
tCtx.fillStyle = isBelowLine ? "#222" : cCode; tCtx.fillRect(x*s+s*0.15, eY-s*0.15, s*0.7, s*0.12);
} else if (colorIdx === 2) { // 水色
tCtx.beginPath(); tCtx.arc(x*s+s*0.3, eY-10, 5, 3.1, 0); tCtx.arc(x*s+s*0.7, eY-10, 5, 3.1, 0); tCtx.stroke();
} else if (colorIdx === 3) { // 黄色
tCtx.beginPath(); tCtx.arc(x*s+s*0.5, eY+12, 6, 0, Math.PI); tCtx.stroke();
}
}
}
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
updateReachStatus();
if (currentMode === 'SEE-THRU' || currentMode === 'EYES ONLY') {
ctx.strokeStyle = "orange"; ctx.setLineDash([8, 4]);
ctx.beginPath(); ctx.moveTo(0, SIZE * 2); ctx.lineTo(canvas.width, SIZE * 2); ctx.stroke(); ctx.setLineDash([]);
}
for(let y=0; y<ROWS; y++) for(let x=0; x<COLS; x++) if(board[y][x]) drawBlock(ctx, x, y, board[y][x]);
if(gameActive && p1) { drawBlock(ctx, p1.x, p1.y, p1.c); drawBlock(ctx, p2.x, p2.y, p2.c); }
if(gameActive) requestAnimationFrame(draw);
}
function move(dx, dy, dr = 0) {
let nr = (rot + dr) % 4, nx = p1.x + dx, ny = p1.y + dy;
const check = (x, y, r) => {
const ox = [0,1,0,-1], oy = [-1,0,1,0], x2 = x + ox[r], y2 = y + oy[r];
return x>=0 && x<COLS && y<ROWS && x2>=0 && x2<COLS && y2<ROWS && (y<0 || board[y][x]===0) && (y2<0 || board[y2][x2]===0);
};
if (check(nx, ny, nr)) { p1.x = nx; p1.y = ny; rot = nr; }
else if (dr !== 0) {
if (check(nx-1, ny, nr)) { p1.x = nx-1; p1.y = ny; rot = nr; }
else if (check(nx+1, ny, nr)) { p1.x = nx+1; p1.y = ny; rot = nr; }
}
const ox = [0,1,0,-1], oy = [-1,0,1,0]; p2.x = p1.x + ox[rot]; p2.y = p1.y + oy[rot];
}
async function gameStep() {
if(!gameActive || !p1) return;
if (canMoveDown()) { p1.y++; p2.y++; }
else {
playSound(150, 'square', 0.1);
board[p1.y][p1.x] = p1.c; if(p2.y >= 0) board[p2.y][p2.x] = p2.c;
p1 = null; reachBlocks = []; await resolveChains();
if(gameActive) {
if(board[1][2] !== 0) { gameOver(); return; }
createNewPuyo();
}
}
}
function canMoveDown() {
const ox = [0,1,0,-1], oy = [-1,0,1,0];
let x2 = p1.x + ox[rot], y2 = p1.y + oy[rot];
return p1.y+1 < ROWS && (p1.y+1 < 0 || board[p1.y+1][p1.x]===0) && y2+1 < ROWS && (y2+1 < 0 || board[y2+1][x2]===0);
}
async function resolveChains() {
let chain = 0;
while(true) {
let moved = false;
for(let x=0; x<COLS; x++) for(let y=ROWS-1; y>0; y--) if(board[y][x]===0 && board[y-1][x]!==0){ board[y][x]=board[y-1][x]; board[y-1][x]=0; moved=true; }
if(moved){ await new Promise(r=>setTimeout(r, 60)); continue; }
let erased = 0, chk = Array.from({length:ROWS},()=>Array(COLS).fill(false));
for(let y=0; y<ROWS; y++) for(let x=0; x<COLS; x++) {
if(board[y][x]!==0 && !chk[y][x]){
let conn = []; findConn(x,y,board[y][x],conn,chk);
if(conn.length>=4){ erased+=conn.length; conn.forEach(p=>board[p.y][p.x]=0); }
}
}
if(erased>0){
chain++; score += erased * 10 * chain; document.getElementById("scoreValue").innerText = score;
let freq = 261.63 * Math.pow(Math.pow(2, 1/12), [0,2,4,5,7,9,11,12][Math.min(chain-1, 7)]);
playSound(freq, 'sine', 0.4, 0.15);
showCombo(chain);
await new Promise(r=>setTimeout(r,350)); continue;
}
break;
}
}
function findConn(x,y,c,conn,chk){
if(x<0||x>=COLS||y<0||y>=ROWS||chk[y][x]||board[y][x]!==c) return;
chk[y][x]=true; conn.push({x,y});
[{dx:1,dy:0},{dx:-1,dy:0},{dx:0,dy:1},{dx:0,dy:-1}].forEach(d=>findConn(x+d.dx,y+d.dy,c,conn,chk));
}
function createNewPuyo() {
p1 = { x: 2, y: 1, c: nextC1 }; p2 = { x: 2, y: 0, c: nextC2 }; rot = 0;
p2.x = p1.x; p2.y = p1.y-1;
nextC1 = COLORS[Math.floor(Math.random()*4)]; nextC2 = COLORS[Math.floor(Math.random()*4)];
nextCtx.clearRect(0,0,100,100); drawBlock(nextCtx, 0.5, 0.6, nextC1, 40, true); drawBlock(nextCtx, 0.5, -0.4, nextC2, 40, true);
}
function gameOver() {
gameActive = false; clearInterval(gameTimer);
let isNewRecord = false;
if (score > highScores[currentMode]) {
highScores[currentMode] = score;
localStorage.setItem('breakBlockBest', JSON.stringify(highScores));
updateBestDisplay();
isNewRecord = true;
}
document.getElementById("overlay").style.display="flex";
document.getElementById("overlay-content").innerHTML = `
${isNewRecord ? '<div class="new-record-msg">NEW RECORD!</div>' : ''}
<div class="title-break">GAME</div><div class="title-block">OVER</div>
<p>SCORE: ${score}</p>
<button class="btn" onclick="location.reload()">BACK TO MENU</button>
`;
}
function startGame(m) {
initAudio();
currentMode=m; board=Array.from({length:ROWS},()=>Array(COLS).fill(0));
score=0; document.getElementById("scoreValue").innerText=0;
document.getElementById("modeValue").innerText=m;
gameActive=true; document.getElementById("overlay").style.display="none";
nextC1=COLORS[0]; nextC2=COLORS[1]; createNewPuyo();
clearInterval(gameTimer); gameTimer = setInterval(gameStep, m === 'HELL' ? 150 : 800);
draw();
}
window.addEventListener("keydown", e => {
if(!gameActive || !p1) return;
if(e.key==="ArrowLeft") { lookDir=-1; move(-1, 0); }
else if(e.key==="ArrowRight") { lookDir=1; move(1, 0); }
else if(e.key==="ArrowDown") gameStep();
else if(e.key==="ArrowUp") { while(canMoveDown()){ p1.y++; p2.y++; } gameStep(); }
else if(e.key===" ") move(0, 0, 1);
});
</script>
</body>
</html>

■ 今日の学び

 作っていくうちに、AIが自分で考えて自動的に修正してしまう場面がありました。 修正を重ねるたびに、良かったところが消えてしまったり、意図しない要素が混ざったり、動作がおかしくなったりして、思うようにいかないことも多かったです。§(T_T、)

 そこで今回は、
「ここまでの仕上がりを崩したくないので、この状態を保ったまま新しい要素を上乗せしていきたいです。」
「ほかに影響が出ないように気を付けて、動作確認OKなら次の修正に進みます。」
「影響が出ないように慎重に設定してみてください。」
 といった感じで、AIに“慎重さ”をお願いしながら進めていきました。

 次回は、電子ゲームの思い出から作ったゲームです。 つづけて頑張ります!よろしくお願いします(*^v^)ノシ

次の実験(電子ゲーム?):「丸太郎と突撃猪」

コメントを残す