
【ゲーム】JavaScript:08 フルーツクラッシュ
この「🍓 フルーツクラッシュゲーム 🍎」は、8×8 のグリッド上で隣接するフルーツをドラッグ&ドロップで入れ替え、同じ絵文字が3つ以上横か縦に並ぶと消去&スコア加算、空いたマスに上からフルーツが落ちてくるパズルゲームです。制限時間 60 秒でどれだけスコアを伸ばせるかを競い、ハイスコアは簡易ランキングに残ります。
🍓 ルール
項目 | 内容 |
---|---|
🎮 目的 | フルーツを入れ替えて、同じ絵柄のフルーツを縦または横に3つ揃えて消す。 |
⏱ 制限時間 | 60秒間の制限時間内で、できるだけ高いスコアを獲得する。 |
💯 得点方法 | 3つ以上の同種フルーツが並んで消えると、スコアが+10点加算 |
🔄 入れ替え | 隣接したフルーツのみ入れ替え可能(上下左右1マス) |
✅ マッチ条件 | 入れ替え後、縦または横に3個以上同じフルーツが揃えば成功 |
⛔ 無効な入れ替え | 揃わなかった場合は元に戻る。 |
⬇️ 補充処理 | 消えたマスには上からフルーツが落ちてきて、新しいフルーツで補充される。 |
🏆 ランキング機能 | ゲーム終了時のスコアが上位5件のスコアとして保存・表示される。 |
🕹️ 遊び方の流れ
- 「▶️ スタート」ボタンをクリック
・8×8のグリッドにランダムなフルーツが表示されてゲームスタート。
・タイマー(60秒)がカウントダウン開始。 - フルーツをドラッグ&ドロップ
・隣り合うフルーツを入れ替えて、同じフルーツを3つ以上揃える。
・揃えると自動で消え、スコアが増える。 - 消えたあと、上からフルーツが補充
・消えたスペースに、上のフルーツが落ち、上部には新しいフルーツが追加される。 - 60秒経過でゲーム終了
・スコアに応じて「Win」または「Game Over」のメッセージが表示される。
・スコアはランキングに記録され、上位5件が表示される。 - 「🔄 リセット」ボタンでリセット
・ゲームボードが非表示になり、スコアや時間が初期状態に戻る。
プレイは画面下の「▶️ スタート」ボタンを押すだけ。グリッドが表示されたら、お好きなフルーツを長押し(ドラッグ)して隣のマスにドロップすることで入れ替えができ、3つ並ぶ組み合わせがあれば自動で消去され新たなフルーツが補充されます。時間内は何度でも組み換え可能ですが、消去できない入れ替えは元に戻ります。
リセットしたいときは「🔄 リセット」で盤面もスコアもタイマーも一気に初期化できます。
3つ以上並べて消去したときだけ得点が入るのがルールのポイント。並びは横3つ/縦3つで判定し、同時に複数列消せばまとめてスコア加算。時間切れになると自動でゲームオーバーとなり、獲得スコアに応じて「300 点以上なら勝利メッセージ、それ未満なら再挑戦メッセージ」が出ます。その後ハイスコア欄にスコアが登録され、上位5件まで表示されます。
🎮ゲームプレイ
以下のリンク先から実際にプレイできます。
08 フルーツクラッシュ
素材のダウンロード
以下のリンクから使用する素材をダウンロードできます。
fruit_crush_bg.png |
---|
![]() |
ゲーム画面イメージ

プログラム全文(fruit_crush.html)
<!DOCTYPE html>
<!--
===========================================================
Fruit Crush Game 🍓🍌🍇🍊🍎
Fully‑commented version (2025‑05‑06)
───────────────────────────────────────────────────────────
・ゲーム内容 :8×8 マスのキャンディクラッシュ系パズル
・主な機能 :ドラッグ&ドロップでフルーツ交換/
3 つ揃うと消去&落下補充/60 秒タイマー/
スコア計算/簡易ランキング表示
===========================================================
-->
<html lang="ja">
<head>
<!-- 文字コードを UTF‑8 に設定(日本語・絵文字どちらも安全に扱える) -->
<meta charset="UTF-8">
<!-- レスポンシブ対応:ビューポートをデバイス幅に合わせる -->
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- ブラウザのタブに表示されるタイトル(絵文字入り) -->
<title>🍓 フルーツクラッシュゲーム 🍎</title>
<!--
--------------------------------------------------------
ここからインライン CSS
‑ ページ全体のレイアウト
‑ ボタン/グリッド/アニメーションなどの装飾
--------------------------------------------------------
-->
<style>
/* ======== 基本レイアウト設定 ======== */
body {
/* Flexbox で縦中央寄せ */
display: flex;
flex-direction: column;
align-items: center;
/* 画面いっぱいの高さを確保 */
height: 100vh;
margin: 0;
/* 背景に繰り返しタイル画像を設定(ゲームの雰囲気アップ) */
background: url('fruit_crush_bg.png') repeat;
background-size: cover;
/* 全体フォント */
font-family: Arial, sans-serif;
}
/* ======== タイトル見出し ======== */
.main-title {
font-size: 2.5rem;
margin: 20px 0;
/* 文字に軽い影を付けて視認性アップ */
text-shadow: 1px 1px 2px rgba(0,0,0,0.3);
}
/* ======== フェードアウト用アニメーション ======== */
.fade-out {
animation: fadeOut 0.5s forwards; /* forwards で最終状態を保持 */
}
@keyframes fadeOut {
from { opacity: 1; }
to { opacity: 0; }
}
/* ======== 落下演出用アニメーション ======== */
.fall-down {
animation: fallDown 0.5s ease-out;
}
@keyframes fallDown {
from { transform: translateY(-50px); }
to { transform: translateY(0); }
}
/* (未使用)背景グラデーションを動かすサンプルアニメ */
@keyframes gradientBackground {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
/* ======== スコア & タイマー表示エリア ======== */
.game-header {
display: flex; /* 左右に並べる */
justify-content: space-between;
width: 400px;
margin-bottom: 10px;
/* 文字装飾 */
font-family: Arial, sans-serif;
font-size: 20px;
font-weight: bold;
color: #fff;
/* 半透明背景で読みやすさ確保 */
background: rgba(0,0,0,0.5);
padding: 4px 8px;
border-radius: 8px;
}
/* ======== ゲーム盤(8×8 グリッド) ======== */
.game-container {
/* CSS Grid で 8 列 × 8 行 を定義 */
display: grid;
grid-template-columns: repeat(8, 50px);
grid-template-rows: repeat(8, 50px);
gap: 2px; /* マス間の隙間 */
background-color: #222; /* 盤のフチ色 */
padding: 5px;
border-radius: 10px;
/* 初期状態では非表示(スタート時に切り替え) */
display: none;
}
/* ======== 各マスに入るフルーツ絵文字 ======== */
.candy {
width: 50px;
height: 50px;
border-radius: 10%; /* 角を軽く丸める */
cursor: pointer; /* ホバー時にポインタ表示 */
/* 中央揃え(Flexbox) */
display: flex;
justify-content: center;
align-items: center;
font-size: 30px; /* 絵文字サイズ */
}
/* ======== 空マス用クラス ======== */
.blank {
background-color: transparent;
}
/* ======== START / RESET ボタン周り ======== */
.button-container {
margin: 20px;
}
.button-container button {
margin: 0 10px;
padding: 10px 20px;
font-size: 16px;
cursor: pointer;
/* ボタンを丸みのあるカラフルなグラデーションに */
border: none;
border-radius: 25px;
background: linear-gradient(45deg, #ff9a9e, #fad0c4);
color: white;
/* 手書き風フォントでポップ感を演出 */
font-family: 'Comic Sans MS', sans-serif;
font-weight: bold;
/* 影で立体感追加 */
box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.2);
transition: all 0.3s ease;
}
.button-container button:hover {
background: linear-gradient(45deg, #fad0c4, #ff9a9e);
transform: scale(1.1);
box-shadow: 0px 6px 8px rgba(0, 0, 0, 0.1);
}
/* ======== ゲームメッセージ表示エリア ======== */
#message-area{
min-height:40px; /* 高さを確保(レイアウトの揺れ防止) */
font-size:20px;
margin:10px 0;
text-shadow:1px 1px 2px #000;
color:#fff;
}
/* ======== 簡易ランキング表示 ======== */
.ranking-container {
margin-top: 20px;
background: rgba(0,0,0,0.1);
font-family: Arial, sans-serif;
font-size: 16px;
text-align: center;
color:#fff;
padding:8px;
border-radius:8px;
}
.ranking-container ul {
list-style: none;
padding: 0;
}
.ranking-container li {
margin: 5px 0;
font-weight: bold;
}
</style>
</head>
<body>
<!-- ================== 画面構成 ================== -->
<!-- ゲームタイトル -->
<h1 class="main-title">🍓🍌🍇 フルーツクラッシュゲーム 🍊🍎</h1>
<!-- スコア & タイマー -->
<div class="game-header">
<div>🎯 スコア:<span id="score">0</span></div>
<div>⏱️ 残り時間:<span id="time">60</span> sec</div>
</div>
<!-- 操作用ボタン -->
<div class="button-container">
<button id="start-button">▶️ スタート</button>
<button id="reset-button">🔄 リセット</button>
</div>
<!-- ゲーム盤 -->
<div class="game-container" id="game"></div>
<!-- メッセージ(勝敗やヒント等) -->
<div id="message-area"></div>
<!-- 簡易ランキング -->
<div class="ranking-container">
<h3>🏆ランキング🏆</h3>
<ul id="ranking-list"></ul>
</div>
<!--
==========================================================
ここからゲームロジック(純粋 JavaScript)
==========================================================
-->
<script>
/* ------------- DOM 要素取得 ------------- */
const grid = document.getElementById('game'); // 盤グリッド
const scoreDisplay = document.getElementById('score'); // スコア表示
const timeDisplay = document.getElementById('time'); // 残り時間表示
const startButton = document.getElementById('start-button'); // スタート
const resetButton = document.getElementById('reset-button'); // リセット
const messageArea = document.getElementById('message-area');// メッセージ
/* ------------- ゲーム定数 ------------- */
const gridSize = 8; // 盤の一辺
const fruits = ['🍎', '🍌', '🍇', '🍊', '🍓']; // 5 種のフルーツ絵文字
/* ------------- ゲーム状態変数 ------------- */
const squares = []; // 盤上の <div> を保存(index=ID)
let score = 0; // 現在スコア
let timeRemaining = 60;// タイマー(秒)
let timerInterval = null; // setInterval の戻り値(時間管理)
let matchInterval = null; // setInterval の戻り値(マッチ判定)
/* =======================================================
ユーティリティ:行・列で「同じフルーツが 3 つ並ぶ」かを検出
‑ マッチ時は対象マスを空にし、アニメ演出も付加
======================================================= */
function checkRowForThree() {
let matchFound = false;
/* 盤を左上→右下へ走査(最後 2 列は 3 連にならないので除外) */
for (let i = 0; i < gridSize * gridSize - 2; i++) {
const rowOfThree = [i, i + 1, i + 2]; // 横 3 連
const decidedFruit = squares[i].textContent; // 基準フルーツ
/* 全 3 マスが同じ絵文字かつ空でないか? */
if (rowOfThree.every(index =>
squares[index].textContent === decidedFruit &&
decidedFruit !== '')) {
/* マッチ! → フェードアウト演出 → 空に */
rowOfThree.forEach(index => {
squares[index].classList.add('fade-out');
setTimeout(() => {
squares[index].textContent = '';
squares[index].classList.remove('fade-out');
}, 500); // 0.5 秒後にリセット
});
matchFound = true;
}
}
return matchFound;
}
/* フルーツを 1 マスずつ下へ落とす(上から補充) */
function moveDown() {
for (let i = 0; i < gridSize * (gridSize - 1); i++) {
/* 下が空なら落下 */
if (squares[i + gridSize].textContent === '') {
squares[i + gridSize].textContent = squares[i].textContent;
squares[i + gridSize].classList.add('fall-down');
squares[i].textContent = '';
setTimeout(() => {
squares[i + gridSize].classList.remove('fall-down');
}, 500);
}
/* 先頭行:空マスはランダム補充 */
const firstRow = Array.from({ length: gridSize }, (_, i) => i);
firstRow.forEach(index => {
if (squares[index].textContent === '') {
const randomFruit = Math.floor(Math.random() * fruits.length);
squares[index].textContent = fruits[randomFruit];
squares[index].classList.add('fall-down');
setTimeout(() => {
squares[index].classList.remove('fall-down');
}, 500);
}
});
}
}
/* =======================================================
ボード生成:8×8 マスを生成しランダム配置
======================================================= */
function createBoard() {
grid.innerHTML = ''; // 既存マスをクリア
squares.length = 0; // 配列も初期化
for (let i = 0; i < gridSize * gridSize; i++) {
const square = document.createElement('div');
const randomFruit = Math.floor(Math.random() * fruits.length);
square.classList.add('candy');
square.textContent = fruits[randomFruit];
/* DnD 用属性 */
square.setAttribute('draggable', true);
square.setAttribute('id', i); // index を ID に
grid.appendChild(square);
squares.push(square);
}
/* --- Drag & Drop イベントを各マスに紐付け --- */
squares.forEach(square => {
square.addEventListener('dragstart', dragStart);
square.addEventListener('dragend', dragEnd);
square.addEventListener('dragover', dragOver);
square.addEventListener('dragenter', dragEnter);
square.addEventListener('dragleave', dragLeave);
square.addEventListener('drop', dragDrop);
});
}
/* =======================================================
Drag & Drop 実装:フルーツ同士を交換
======================================================= */
let fruitBeingDragged; // ドラッグ元の絵文字
let fruitBeingReplaced; // ドロップ先の絵文字
let squareIdBeingDragged; // ドラッグ元 ID
let squareIdBeingReplaced; // ドロップ先 ID
/* --- ドラッグ開始 --- */
function dragStart() {
fruitBeingDragged = this.textContent;
squareIdBeingDragged = parseInt(this.id);
}
/* --- ドラッグ中(デフォルト動作を抑止) --- */
function dragOver(e) { e.preventDefault(); }
function dragEnter(e) { e.preventDefault(); }
function dragLeave() { /* 使わないが定義だけ残す */ }
/* --- ドロップ発生 --- */
function dragDrop() {
fruitBeingReplaced = this.textContent;
squareIdBeingReplaced = parseInt(this.id);
/* 2 つの絵文字を入れ替え */
squares[squareIdBeingDragged].textContent = fruitBeingReplaced;
squares[squareIdBeingReplaced].textContent = fruitBeingDragged;
}
/* --- ドラッグ完了 → 入れ替えが有効か判定 --- */
function dragEnd() {
/* 交換できるのは上下左右 1 マスのみ */
const validMoves = [
squareIdBeingDragged - 1,
squareIdBeingDragged + 1,
squareIdBeingDragged - gridSize,
squareIdBeingDragged + gridSize
];
const validMove = validMoves.includes(squareIdBeingReplaced);
/* 交換後に 3 連ができるか? */
const isAValidMatch = checkForMatches();
if (squareIdBeingReplaced && validMove && isAValidMatch) {
/* 成功:スコア加算 */
squareIdBeingReplaced = null;
score += 10;
scoreDisplay.textContent = score;
} else {
/* 失敗:元に戻す */
squares[squareIdBeingDragged].textContent = fruitBeingDragged;
squares[squareIdBeingReplaced].textContent = fruitBeingReplaced;
}
}
/* -------------------------------------------------------
行・列マッチ判定(※ 同名関数が 2 回定義されているが、
ソースを変更しない指示のためそのままにしている)
------------------------------------------------------- */
function checkRowForThree() {
let matchFound = false;
for (let i = 0; i < gridSize * gridSize - 2; i++) {
const rowOfThree = [i, i + 1, i + 2];
const decidedFruit = squares[i].textContent;
if (rowOfThree.every(index =>
squares[index].textContent === decidedFruit &&
decidedFruit !== '')) {
rowOfThree.forEach(index => squares[index].textContent = '');
matchFound = true;
}
}
return matchFound;
}
function checkColumnForThree() {
let matchFound = false;
for (let i = 0; i < gridSize * (gridSize - 2); i++) {
const columnOfThree = [i, i + gridSize, i + gridSize * 2];
const decidedFruit = squares[i].textContent;
if (columnOfThree.every(index =>
squares[index].textContent === decidedFruit &&
decidedFruit !== '')) {
columnOfThree.forEach(index => squares[index].textContent = '');
matchFound = true;
}
}
return matchFound;
}
/* 行または列どちらかでマッチすれば true */
function checkForMatches() {
const rowMatch = checkRowForThree();
const columnMatch = checkColumnForThree();
return rowMatch || columnMatch;
}
/* --- 旧バージョンの moveDown / checkMatches(残置) --- */
function moveDownLegacy() {
for (let i = 0; i < gridSize * (gridSize - 1); i++) {
if (squares[i + gridSize].textContent === '') {
squares[i + gridSize].textContent = squares[i].textContent;
squares[i].textContent = '';
}
const firstRow = Array.from({ length: gridSize }, (_, i) => i);
firstRow.forEach(index => {
if (squares[index].textContent === '') {
const randomFruit = Math.floor(Math.random() * fruits.length);
squares[index].textContent = fruits[randomFruit];
}
});
}
}
function checkMatchesLegacy() {
checkRowForThree();
checkColumnForThree();
moveDownLegacy();
}
/* =======================================================
ランキング機能:ハイスコア Top5 を保持
======================================================= */
const highScores = [];
function updateRanking() {
/* 新スコアを配列に追加し降順ソート */
highScores.push(score);
highScores.sort((a, b) => b - a);
/* 6 件以上なら末尾を削除 */
if (highScores.length > 5) highScores.pop();
/* UL タグを再描画 */
const rankingList = document.getElementById('ranking-list');
rankingList.innerHTML = '';
highScores.forEach((highScore, index) => {
const listItem = document.createElement('li');
listItem.textContent = `${index + 1}位. ${highScore}点`;
rankingList.appendChild(listItem);
});
}
/* =======================================================
タイマー更新:1 秒ごとに呼ばれる
======================================================= */
function updateTimer() {
timeRemaining -= 1;
timeDisplay.textContent = timeRemaining;
if (timeRemaining <= 0) {
/* タイムアップ → すべての定期処理を停止 */
clearInterval(timerInterval);
clearInterval(matchInterval);
/* ランキング更新 */
updateRanking();
/* 結果メッセージ */
const msg = (score >= 300)
? `🎉 おめでとう! スコア ${score} 点で勝利!`
: `🍀 ゲームオーバー… スコア ${score} 点。また挑戦してね!`;
showMessage(msg);
}
}
/* ちょっとしたメッセージ出力ユーティリティ */
function showMessage(text){
messageArea.textContent = text;
}
/* =======================================================
ゲーム開始(Start ボタン)
======================================================= */
function startGame() {
/* 状態を初期化 */
score = 0;
timeRemaining = 60;
scoreDisplay.textContent = score;
timeDisplay.textContent = timeRemaining;
createBoard(); // ボード生成
grid.style.display = 'grid'; // 盤を表示
/* 既存インターバルを念のためクリア */
clearInterval(timerInterval);
clearInterval(matchInterval);
/* 1 秒ごとにタイマー更新 */
timerInterval = setInterval(updateTimer, 1000);
/* 0.1 秒ごとにマッチ判定&落下処理 */
matchInterval = setInterval(() => {
checkRowForThree();
checkColumnForThree();
moveDown();
}, 100);
}
/* =======================================================
ゲームリセット(Reset ボタン)
======================================================= */
function resetGame() {
/* インターバル停止 */
clearInterval(timerInterval);
clearInterval(matchInterval);
/* ステータス初期化 */
score = 0;
timeRemaining = 60;
scoreDisplay.textContent = score;
timeDisplay.textContent = timeRemaining;
/* ボード非表示&クリア */
grid.innerHTML = '';
grid.style.display = 'none';
}
/* ボタンにイベント登録 */
startButton.addEventListener('click', startGame);
resetButton.addEventListener('click', resetGame);
</script>
</body>
</html>
アルゴリズムの流れ
ステップ | 関数/命令 | 内容 |
---|---|---|
ボード生成 | createBoard() | 8×8 の <div> を生成し、ランダムフルーツを配置。ドラッグ&ドロップのイベントを登録。 |
ドラッグ&ドロップ | dragStart /dragDrop /dragEnd | 交換元・先のフルーツを入れ替え、隣接チェック&マッチ判定後にスコア加算 or 元に戻す。 |
マッチ判定 | checkRowForThree() ・checkColumnForThree() | 横3連/縦3連を走査し、消去(フェードアニメ付き)→空マス設定。 |
落下&補充 | moveDown() | 下が空いたら上のフルーツを落とし、最上段はランダム補充。落下アニメ付き。 |
タイマー&マッチループ | setInterval(updateTimer,1000) setInterval(...,100) | 1秒ごとに残り時間更新、0 で終了処理。0.1秒ごとにマッチ&落下処理を実行。 |
ランキング更新 | updateRanking() | 最高5件までスコアを降順で保持し、HTML リストに再描画。 |
関数の詳細
関数名 | 役割 |
---|---|
createBoard() | グリッドを初期化し、フルーツ絵文字とドラッグイベントを各マスに設定 |
dragStart() | ドラッグ開始時に選んだフルーツと ID を記録 |
dragDrop() | ドロップ先でフルーツを入れ替える |
dragEnd() | 入れ替え後「隣接か」「3連マッチか」をチェックし、有効ならスコア加算、無効なら元に戻す |
checkRowForThree() | すべての横3連位置を走査し、連続して同じ絵文字なら消去 |
checkColumnForThree() | 列方向の3連判定 |
moveDown() | 盤上の空マスに対し上のセルを下げ、最上段をランダム補充 |
updateTimer() | 1秒ごとに残り時間を減らし、タイムアップで終了処理とランキング更新 |
startGame() | スコア/時間初期化→ボード生成→インターバル開始→盤面表示 |
resetGame() | インターバル停止→スコア・時間をリセット→盤面非表示 |
改造のポイント
- 4連以上の消去判定:3連だけでなく4連・5連用の最適化を追加し、高得点コンボを演出。
- 特殊アイテム:縦一列全消去・指定フルーツ全消去などのパワーアップブロックを導入。
- コンボボーナス:連鎖的に消えた回数でスコア倍率を変化させるエフェクトを追加。
- レベルアップ:時間経過やスコアに応じて盤面サイズを変えたり、フルーツの落下速度を高速化。
- オンライン対戦:WebSocket でリアルタイム対戦対応し、互いの盤面を見ながらスコアを競う機能。
- モバイル最適化:タッチスワイプで交換、レスポンシブ対応などスマホ向け UI 改良。
- サウンド&演出:消去・落下・タイムアップ時の効果音やもっと派手なアニメーションを追加。
ぜひこの基本構造をベースに、自分だけのオリジナル「フルーツクラッシュ」を作り込んでみてください!