ブロックパズルのソースコード全文 — JavaScript・HTML・CSS

▶ ブロックパズルで遊ぶ 📖 作り方を1から学ぶ

このページでは、ひなテックGamesで実際に動いている「ブロックパズル」のソースコードを丸ごと公開しています。ここに載っているのは説明用に書き直した特別なコードではなく、いまあなたが遊んでいるゲームそのものを動かしている本物のファイルです。

ブロックパズルは7種類のブロック(テトロミノ)を回転・移動させてラインをそろえる、テトリス風の世界中で愛されているパズルゲームです。「マス目をデータでどう持つか」「ブロックを回転させるとはどういうことか」「ぶつかり判定をどうやるか」という、ゲーム作りの大事な考え方がぎっしり詰まっています。

コードはコピーして自分のパソコンで自由に動かせます。「どうやってブロックが動いているんだろう?」と思ったら、解説を読みながらコードを追いかけてみてください。

ブロックパズルは3つのファイルでできている

ブロックパズルは、次のファイルが役割を分担して動いています。それぞれの中身は下で1つずつ解説します。

ファイル役割
index.htmlゲーム画面の骨組み。盤面・スコア表示・ボタンなどの部品をHTMLで配置する
style.css見た目のデザイン。色・大きさ・レイアウト・アニメーション
game.jsゲームのルールを動かす頭脳。操作の受け付け・判定・スコア計算など

この「HTMLで構造、CSSで見た目、JavaScriptで動き」という分担は、ほとんどのWebページ・Webゲームに共通する考え方です。

index.html — ゲーム画面の骨組み

index.htmlは、ブラウザが最初に読み込むファイルです。ここにはゲームの「部品」をHTMLで並べているだけで、動きそのものは書かれていません。

大きく分けると、(1)スコア・ベスト・レベルを並べるscore-area、(2)ホールド表示・ゲーム盤・ネクスト表示を横に並べたtetris-area、(3)スマホ用の操作ボタンmobile-controls、でできています。注目してほしいのはtetris-boardの中が空っぽなことです。盤面のマス(200個)はJavaScriptがあとから作って入れます。スタート画面とゲームオーバー画面はgame-overlayとして盤の上に重ねてあります。

ゲーム本体は<body>の中から始まります。

※ 下のコードでは、ゲームの動きと関係のない広告・アクセス解析用のタグ(ページ先頭の数行)を省いています。

index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
  <title>ブロックパズル|無料ブラウザゲーム|ひなテックGames</title>
  <meta name="description" content="ブロックパズルは落ちてくるブロックを回転・移動させてラインをそろえて消す、テトリス風の無料パズルゲーム。PC・スマホ対応のブラウザゲーム。">
  <link rel="canonical" href="https://hinata-ya.tech/games/games/tetris/">
  <!-- OGP -->
  <meta property="og:title" content="ブロックパズル|無料ブラウザゲーム|ひなテックGames">
  <meta property="og:description" content="ブロックパズルは落ちてくるブロックを回転・移動させてラインをそろえて消す、テトリス風の無料パズルゲーム。PC・スマホ対応のブラウザゲーム。">
  <meta property="og:type" content="website">
  <meta property="og:url" content="https://hinata-ya.tech/games/games/tetris/">
  <meta property="og:site_name" content="ひなテックGames">
  <meta property="og:locale" content="ja_JP">
  <!-- 構造化データ -->
  <script type="application/ld+json">
  {
    "@context": "https://schema.org",
    "@type": "Game",
    "name": "ブロックパズル",
    "description": "落ちてくるブロックを回転・移動させてラインを消す、テトリス風のクラシックなパズルゲーム。",
    "url": "https://hinata-ya.tech/games/games/tetris/",
    "genre": ["パズル", "クラシック"],
    "gamePlatform": "Web Browser",
    "publisher": {
      "@type": "Organization",
      "name": "ひなテック",
      "url": "https://hinata-ya.tech"
    }
  }
  </script>
  <link rel="stylesheet" href="../../css/style.css">
  <link rel="stylesheet" href="style.css?v=20260516">
  <link rel="stylesheet" href="../../css/leaderboard.css">
</head>
<body>
  <div id="header"></div>

  <div class="game-page">
    <h1 class="game-page-title">ブロックパズル</h1>
    <div class="game-page-tags">
      <span class="tag">パズル</span>
      <span class="tag">クラシック</span>
    </div>

    <div class="game-container">
      <!-- スコアエリア -->
      <div class="score-area">
        <div class="score-box">
          <span class="score-label">スコア</span>
          <span class="score-value" id="score">0</span>
        </div>
        <div class="score-box">
          <span class="score-label">ベスト</span>
          <span class="score-value" id="best-score">0</span>
        </div>
        <div class="score-box">
          <span class="score-label">レベル</span>
          <span class="score-value" id="level">1</span>
        </div>
        <button class="btn-new-game" id="btn-new-game">New Game</button>
      </div>

      <!-- ゲームエリア(ホールド + ボード + ネクスト表示) -->
      <div class="tetris-area">
        <!-- ホールドピース表示 -->
        <div class="hold-piece-panel">
          <div class="hold-piece-label">ホールド</div>
          <div class="hold-piece-box" id="hold-piece-box"></div>
        </div>

        <!-- ゲームボード -->
        <div class="board-wrapper">
          <div class="tetris-board" id="tetris-board"></div>

          <!-- ゲームオーバーオーバーレイ -->
          <div class="game-overlay" id="game-over-overlay">
            <div class="overlay-content">
              <h2>ゲームオーバー</h2>
              <p>スコア: <span id="final-score">0</span></p>
              <p class="best-result" id="best-result"></p>
              <button class="btn-primary" id="btn-retry">もう一度遊ぶ</button>
            </div>
          </div>

          <!-- スタートオーバーレイ -->
          <div class="game-overlay active" id="game-start-overlay">
            <div class="overlay-content">
              <h2>ブロックパズル</h2>
              <p>ブロックを並べてラインを消そう!</p>
              <button class="btn-primary" id="btn-start">ゲームスタート</button>
            </div>
          </div>
        </div>

        <!-- ネクストピース表示 -->
        <div class="next-piece-panel">
          <div class="next-piece-label">つぎ</div>
          <div class="next-piece-box" id="next-piece-box"></div>
        </div>
      </div>

      <!-- モバイル操作ボタン -->
      <div class="mobile-controls" id="mobile-controls">
        <div class="mobile-controls-row">
          <button class="mobile-btn" id="btn-hold" aria-label="ホールド">&#9744; ホールド</button>
          <button class="mobile-btn" id="btn-rotate" aria-label="回転">&#10227;</button>
        </div>
        <div class="mobile-controls-row">
          <button class="mobile-btn" id="btn-left" aria-label="左">&#9664;</button>
          <button class="mobile-btn" id="btn-down" aria-label="下">&#9660;</button>
          <button class="mobile-btn" id="btn-right" aria-label="右">&#9654;</button>
        </div>
        <div class="mobile-controls-row">
          <button class="mobile-btn mobile-btn-wide" id="btn-drop" aria-label="ハードドロップ">&#9660;&#9660; ドロップ</button>
        </div>
      </div>
    </div>

    <div class="game-instructions">
      <p>「ブロックパズル」は、落ちてくるブロックを回転・移動させてラインをそろえて消す、テトリス風の無料パズルゲームです。</p>
      <h3>遊び方</h3>
      <ul>
        <li><strong>PC</strong>: ← → で左右移動、↑ で回転、↓ でソフトドロップ、スペースでハードドロップ、Shift/Cでホールド</li>
        <li><strong>スマホ</strong>: 画面下のボタンで操作</li>
        <li>横一列をブロックで埋めるとラインが消えます</li>
        <li>一度に多くのラインを消すと高得点!(1列=100, 2列=300, 3列=500, 4列=800)</li>
        <li>10ライン消すごとにレベルアップ!スピードも上がります</li>
        <li>ブロックが一番上まで積み上がるとゲームオーバー!</li>
      </ul>
    </div>

    <div class="game-promo">
      <h3>このゲーム、自分でも作れるよ!</h3>
      <p>
        ブロックパズルはJavaScriptの配列操作と<br>
        タイマー処理で作られています。<br><br>
        ひなテックでは、こうしたゲームの<br>
        作り方を楽しく学べます!
      </p>
      <div class="promo-links">
        <a href="tutorial/" class="btn-primary">このゲームの作り方を見る →</a>
        <a href="https://hinata-ya.tech/contact/" class="btn-primary">無料体験に申し込む →</a>
        <a href="source/" class="btn-secondary">ソースコードを見る →</a>
      </div>
    </div>

    <div class="other-games">
      <a href="../../index.html">&larr; 他のゲームも遊ぶ</a>
    </div>
  </div>

  <div id="footer"></div>

  <script src="../../js/common.js?v=20260317" data-base="../../"></script>
  <script src="../../js/leaderboard.js"></script>
  <script src="game.js?v=20260516"></script>
</body>
</html>

style.css — 見た目とアニメーション

style.cssは、ブロックパズルの見た目を作っているファイルです。7種類のブロックはそれぞれ色が違いますが、その色は.cell-I(むらさき)から.cell-L(赤)までのクラスで1つずつ指定しています。JavaScriptは「このマスはJ型の色」と決めるとき、マスにcell-Jというクラスを付けるだけで色がつきます。

.cell-ghostは「ゴーストピース」という、ブロックが落ちる先をうすく見せるための専用スタイルです。ライン消去のときの光る演出は.clearingクラスのアニメーションで作っています。

ゲーム盤はdisplay:gridで10列のマス目に並べています。ファイルの下のほうの@mediaはスマホ向けにマスを小さくする指定です。

style.css
/* ============================================
   ブロックパズル - 固有スタイル
   ============================================ */

/* スコアエリア */
.score-area {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  margin-bottom: 1rem;
  flex-wrap: wrap;
}

.score-box {
  background: #BBADA0;
  border-radius: 8px;
  padding: 0.5rem 1rem;
  text-align: center;
  min-width: 70px;
}

.score-label {
  display: block;
  font-size: 0.7rem;
  color: #EEE4DA;
  text-transform: uppercase;
  letter-spacing: 0.05em;
}

.score-value {
  display: block;
  font-size: 1.25rem;
  font-weight: 700;
  color: #fff;
}

.btn-new-game {
  margin-left: auto;
  background: #8F7A66;
  color: #fff;
  border: none;
  border-radius: 8px;
  padding: 0.5rem 1.25rem;
  font-size: 0.85rem;
  font-weight: 600;
  cursor: pointer;
  transition: background 0.2s;
}

.btn-new-game:hover {
  background: #7A6658;
}

/* ブロックエリア(ボード + ネクスト) */
.tetris-area {
  display: flex;
  gap: 1rem;
  justify-content: center;
  align-items: flex-start;
}

/* ゲームボード */
.board-wrapper {
  position: relative;
  width: 100%;
  max-width: 300px;
  aspect-ratio: 1 / 2;
  touch-action: none;
}

.tetris-board {
  position: absolute;
  inset: 0;
  display: grid;
  grid-template-columns: repeat(10, 1fr);
  grid-template-rows: repeat(20, 1fr);
  gap: 1px;
  padding: 4px;
  background: #2C2C2E;
  border-radius: 8px;
  overflow: hidden;
}

.tetris-cell {
  background: #1C1C1E;
  border-radius: 2px;
}

/* ブロックの色(固定ブロック) */
.tetris-cell.cell-I { background: #8B5CF6; }
.tetris-cell.cell-O { background: #14B8A6; }
.tetris-cell.cell-T { background: #F59E0B; }
.tetris-cell.cell-S { background: #EC4899; }
.tetris-cell.cell-Z { background: #38BDF8; }
.tetris-cell.cell-J { background: #84CC16; }
.tetris-cell.cell-L { background: #EF4444; }

/* ゴーストピース */
.tetris-cell.cell-ghost {
  background: rgba(255, 255, 255, 0.12);
  border: 1px solid rgba(255, 255, 255, 0.2);
}

/* ホールドピースパネル */
.hold-piece-panel {
  flex-shrink: 0;
  width: 90px;
}

.hold-piece-label {
  font-size: 0.75rem;
  font-weight: 600;
  color: #EEE4DA;
  text-align: center;
  background: #BBADA0;
  border-radius: 8px 8px 0 0;
  padding: 0.3rem 0.5rem;
  letter-spacing: 0.05em;
}

.hold-piece-box {
  background: #2C2C2E;
  border-radius: 0 0 8px 8px;
  padding: 10px;
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  grid-template-rows: repeat(4, 1fr);
  gap: 2px;
  aspect-ratio: 1 / 1;
}

.hold-piece-box.hold-locked {
  opacity: 0.4;
}

.hold-cell {
  background: #1C1C1E;
  border-radius: 2px;
}

.hold-cell.cell-I { background: #8B5CF6; }
.hold-cell.cell-O { background: #14B8A6; }
.hold-cell.cell-T { background: #F59E0B; }
.hold-cell.cell-S { background: #EC4899; }
.hold-cell.cell-Z { background: #38BDF8; }
.hold-cell.cell-J { background: #84CC16; }
.hold-cell.cell-L { background: #EF4444; }

/* ネクストピースパネル */
.next-piece-panel {
  flex-shrink: 0;
  width: 90px;
}

.next-piece-label {
  font-size: 0.75rem;
  font-weight: 600;
  color: #EEE4DA;
  text-align: center;
  background: #BBADA0;
  border-radius: 8px 8px 0 0;
  padding: 0.3rem 0.5rem;
  letter-spacing: 0.05em;
}

.next-piece-box {
  background: #2C2C2E;
  border-radius: 0 0 8px 8px;
  padding: 10px;
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  grid-template-rows: repeat(4, 1fr);
  gap: 2px;
  aspect-ratio: 1 / 1;
}

.next-cell {
  background: #1C1C1E;
  border-radius: 2px;
}

.next-cell.cell-I { background: #8B5CF6; }
.next-cell.cell-O { background: #14B8A6; }
.next-cell.cell-T { background: #F59E0B; }
.next-cell.cell-S { background: #EC4899; }
.next-cell.cell-Z { background: #38BDF8; }
.next-cell.cell-J { background: #84CC16; }
.next-cell.cell-L { background: #EF4444; }

/* ゲームオーバーレイ */
.game-overlay {
  position: absolute;
  inset: 0;
  background: rgba(44, 44, 46, 0.9);
  border-radius: 8px;
  display: none;
  align-items: center;
  justify-content: center;
  z-index: 10;
  animation: overlay-appear 0.3s ease;
}

.game-overlay.active {
  display: flex;
}

@keyframes overlay-appear {
  0% {
    opacity: 0;
  }
  100% {
    opacity: 1;
  }
}

.overlay-content {
  text-align: center;
  padding: 1.5rem;
}

.overlay-content h2 {
  font-size: 1.75rem;
  color: #fff;
  margin-bottom: 0.5rem;
}

.overlay-content p {
  font-size: 1.1rem;
  color: #ddd;
  margin-bottom: 0.75rem;
}

.overlay-content .best-result {
  font-size: 0.95rem;
  color: #FFEB3B;
  font-weight: 600;
  margin-bottom: 1rem;
}

.overlay-content .btn-primary {
  margin-bottom: 0.5rem;
}

/* モバイル操作ボタン */
.mobile-controls {
  display: none;
  margin: 1.25rem auto 0;
  max-width: 280px;
}

/* PCでは非表示 */
@media (min-width: 769px) {
  .mobile-controls {
    display: none;
  }
}

/* モバイルでは表示 */
@media (max-width: 768px) {
  .mobile-controls {
    display: block;
  }
}

.mobile-controls-row {
  display: flex;
  justify-content: center;
  gap: 8px;
  margin-bottom: 8px;
}

.mobile-btn {
  width: 72px;
  height: 52px;
  border: none;
  border-radius: 12px;
  background: #8F7A66;
  color: #fff;
  font-size: 1.25rem;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: background 0.15s, transform 0.1s;
  user-select: none;
  -webkit-user-select: none;
  -webkit-tap-highlight-color: transparent;
}

.mobile-btn:active {
  background: #7A6658;
  transform: scale(0.92);
}

.mobile-btn-wide {
  width: 100%;
  max-width: 232px;
  font-size: 0.95rem;
  font-weight: 600;
  gap: 0.25rem;
}

/* ライン消去アニメーション */
@keyframes line-clear {
  0% {
    background: #fff;
    transform: scaleY(1);
  }
  50% {
    background: #FFEB3B;
  }
  100% {
    background: #fff;
    transform: scaleY(0);
    opacity: 0;
  }
}

.tetris-cell.clearing {
  animation: line-clear 0.3s ease forwards;
}

/* レスポンシブ */
@media (max-width: 500px) {
  .tetris-area {
    gap: 0.5rem;
  }

  .board-wrapper {
    max-width: 250px;
  }

  .hold-piece-panel {
    width: 72px;
  }

  .hold-piece-box {
    padding: 6px;
  }

  .next-piece-panel {
    width: 72px;
  }

  .next-piece-box {
    padding: 6px;
  }

  .tetris-board {
    gap: 1px;
    padding: 3px;
  }

  .tetris-cell {
    border-radius: 1px;
  }
}

@media (max-width: 360px) {
  .board-wrapper {
    max-width: 210px;
  }

  .hold-piece-panel {
    width: 60px;
  }

  .next-piece-panel {
    width: 60px;
  }

  .tetris-board {
    gap: 0px;
    padding: 2px;
  }

  .score-box {
    padding: 0.4rem 0.75rem;
    min-width: 55px;
  }

  .score-value {
    font-size: 1rem;
  }

  .mobile-btn {
    width: 60px;
    height: 46px;
    border-radius: 10px;
    font-size: 1.1rem;
  }

  .mobile-btn-wide {
    max-width: 196px;
    font-size: 0.85rem;
  }
}

game.js — ゲームのルールを動かす頭脳

game.jsがこのゲームの心臓部です。少し長いですが、やっていることを順番に追えば必ず理解できます。全体は(function () { ... })();という「即時実行関数」で囲まれ、ゲームの変数が他のコードとぶつからないようになっています。

まず大事なのがboardという20行×10列の2次元配列です。board[y][x]が空文字ならそのマスは空き、色名('I'など)が入っていればブロックが固定されています。落下中のブロックはcurrentPieceという別のオブジェクトで、種類・回転状態・位置(x, y)を持っています。

このゲームでいちばん面白いのがTETROMINOSというブロックの設計図です。7種類それぞれに、回転した4つの形を「ブロックを構成する4マスの座標リスト」として書き並べています。だから「回転する」というのは難しい計算をしているわけではなく、rotationという番号を(rotation + 1) % 4で次に進めて、別の形のリストに切りかえているだけなのです。

ブロックが動けるかどうかはisValidPosition()が判定します。ブロックの4マスを1つずつ見て、盤の外に出ていないか、すでにあるブロックとぶつからないかを調べます。回転したいのに壁が邪魔なとき、左右に少しずらしてもう一度試すrotatePiece()の中の「ウォールキック」も、ブロックパズルならではの気持ちよさを生む工夫です。

横一列がそろったかはclearLines()が調べます。そろった行はboard.splice()で配列から抜き取り、かわりに空っぽの行をboard.unshift()で上に足します。これで上のブロックがストンと落ちます。消した列数に応じてLINE_SCORESから得点を決め、4列同時消しがいちばん高得点です。

自動で落下させているのはgameStep()です。setTimeoutcurrentSpeedミリ秒ごとに自分をまた呼び出し、1マスずつ下げています。レベルが上がるとcalcSpeed()でこの間隔が短くなり、ブロックが速く落ちてきます。接地してすぐ固定せず、LOCK_DELAYのあいだ動かせる「ロック猶予」も本格的な仕組みです。ベストスコアはlocalStorageに保存されます。

game.js
// ============================================
// ブロックパズル ロジック
// ============================================

(function () {
  'use strict';

  // ============================================
  // 定数
  // ============================================
  var COLS = 10;              // 横のマス数
  var ROWS = 20;              // 縦のマス数
  var INITIAL_SPEED = 800;    // 初期落下速度(ミリ秒)
  var MIN_SPEED = 80;         // 最速(ミリ秒)
  var SPEED_DECREASE = 60;    // レベルごとの速度減少量
  var LINES_PER_LEVEL = 10;  // レベルアップに必要なライン数
  var LOCK_DELAY = 500;       // 接地後のロック猶予(ミリ秒)

  // スコアテーブル(消去ライン数 → 得点)
  var LINE_SCORES = {
    1: 100,
    2: 300,
    3: 500,
    4: 800
  };

  // テトロミノの定義(4つの回転状態)
  var TETROMINOS = {
    I: {
      shapes: [
        [[0,0],[1,0],[2,0],[3,0]],
        [[0,0],[0,1],[0,2],[0,3]],
        [[0,0],[1,0],[2,0],[3,0]],
        [[0,0],[0,1],[0,2],[0,3]]
      ],
      color: 'I'
    },
    O: {
      shapes: [
        [[0,0],[1,0],[0,1],[1,1]],
        [[0,0],[1,0],[0,1],[1,1]],
        [[0,0],[1,0],[0,1],[1,1]],
        [[0,0],[1,0],[0,1],[1,1]]
      ],
      color: 'O'
    },
    T: {
      shapes: [
        [[0,0],[1,0],[2,0],[1,1]],
        [[0,0],[0,1],[0,2],[1,1]],
        [[1,0],[0,1],[1,1],[2,1]],
        [[1,0],[1,1],[1,2],[0,1]]
      ],
      color: 'T'
    },
    S: {
      shapes: [
        [[1,0],[2,0],[0,1],[1,1]],
        [[0,0],[0,1],[1,1],[1,2]],
        [[1,0],[2,0],[0,1],[1,1]],
        [[0,0],[0,1],[1,1],[1,2]]
      ],
      color: 'S'
    },
    Z: {
      shapes: [
        [[0,0],[1,0],[1,1],[2,1]],
        [[1,0],[0,1],[1,1],[0,2]],
        [[0,0],[1,0],[1,1],[2,1]],
        [[1,0],[0,1],[1,1],[0,2]]
      ],
      color: 'Z'
    },
    J: {
      shapes: [
        [[0,0],[0,1],[1,1],[2,1]],
        [[0,0],[1,0],[0,1],[0,2]],
        [[0,0],[1,0],[2,0],[2,1]],
        [[1,0],[1,1],[0,2],[1,2]]
      ],
      color: 'J'
    },
    L: {
      shapes: [
        [[2,0],[0,1],[1,1],[2,1]],
        [[0,0],[0,1],[0,2],[1,2]],
        [[0,0],[1,0],[2,0],[0,1]],
        [[0,0],[1,0],[1,1],[1,2]]
      ],
      color: 'L'
    }
  };

  var PIECE_NAMES = ['I', 'O', 'T', 'S', 'Z', 'J', 'L'];

  // ============================================
  // 状態変数
  // ============================================
  var board = [];               // ボード配列 [row][col] = '' or color名
  var currentPiece = null;      // 現在のピース { type, rotation, x, y }
  var nextPieceType = null;     // 次のピースの種類
  var holdPieceType = null;     // ホールド中のピースの種類
  var holdUsed = false;         // 現在のピースでホールド済みか
  var score = 0;
  var bestScore = parseInt(localStorage.getItem('bestTetris') || '0', 10);
  var level = 1;
  var totalLines = 0;
  var gameRunning = false;
  var gameOverFlag = false;
  var gameLoopTimer = null;
  var currentSpeed = INITIAL_SPEED;
  var lockTimer = null;
  var isLocking = false;

  // ============================================
  // DOM要素
  // ============================================
  var boardEl = document.getElementById('tetris-board');
  var scoreEl = document.getElementById('score');
  var bestScoreEl = document.getElementById('best-score');
  var levelEl = document.getElementById('level');
  var gameOverOverlay = document.getElementById('game-over-overlay');
  var gameStartOverlay = document.getElementById('game-start-overlay');
  var finalScoreEl = document.getElementById('final-score');
  var bestResultEl = document.getElementById('best-result');
  var nextPieceBox = document.getElementById('next-piece-box');
  var holdPieceBox = document.getElementById('hold-piece-box');

  // セルの2次元配列(高速アクセス用)
  var cells = [];
  var nextCells = [];
  var holdCells = [];

  // ============================================
  // ボードの初期化(DOMセルを生成)
  // ============================================
  function createBoard() {
    boardEl.innerHTML = '';
    cells = [];

    for (var y = 0; y < ROWS; y++) {
      cells[y] = [];
      for (var x = 0; x < COLS; x++) {
        var cell = document.createElement('div');
        cell.className = 'tetris-cell';
        boardEl.appendChild(cell);
        cells[y][x] = cell;
      }
    }
  }

  function createNextPieceDisplay() {
    nextPieceBox.innerHTML = '';
    nextCells = [];

    for (var y = 0; y < 4; y++) {
      nextCells[y] = [];
      for (var x = 0; x < 4; x++) {
        var cell = document.createElement('div');
        cell.className = 'next-cell';
        nextPieceBox.appendChild(cell);
        nextCells[y][x] = cell;
      }
    }
  }

  function createHoldPieceDisplay() {
    holdPieceBox.innerHTML = '';
    holdCells = [];

    for (var y = 0; y < 4; y++) {
      holdCells[y] = [];
      for (var x = 0; x < 4; x++) {
        var cell = document.createElement('div');
        cell.className = 'hold-cell';
        holdPieceBox.appendChild(cell);
        holdCells[y][x] = cell;
      }
    }
  }

  // ============================================
  // ボードデータ初期化
  // ============================================
  function initBoard() {
    board = [];
    for (var y = 0; y < ROWS; y++) {
      board[y] = [];
      for (var x = 0; x < COLS; x++) {
        board[y][x] = '';
      }
    }
  }

  // ============================================
  // ランダムなテトロミノを生成
  // ============================================
  function randomPieceType() {
    return PIECE_NAMES[Math.floor(Math.random() * PIECE_NAMES.length)];
  }

  // ============================================
  // 新しいピースを生成
  // ============================================
  function spawnPiece() {
    var type = nextPieceType || randomPieceType();
    nextPieceType = randomPieceType();
    holdUsed = false;

    var piece = {
      type: type,
      rotation: 0,
      x: Math.floor(COLS / 2) - 1,
      y: 0
    };

    // I型は少し左にずらす
    if (type === 'I') {
      piece.x = Math.floor(COLS / 2) - 2;
    }

    // 設置できるか確認
    if (!isValidPosition(piece)) {
      // ゲームオーバー
      return null;
    }

    return piece;
  }

  // ============================================
  // ピースの現在の形状を取得
  // ============================================
  function getShape(piece) {
    return TETROMINOS[piece.type].shapes[piece.rotation];
  }

  // ============================================
  // ピースの位置が有効か判定
  // ============================================
  function isValidPosition(piece) {
    var shape = getShape(piece);

    for (var i = 0; i < shape.length; i++) {
      var newX = piece.x + shape[i][0];
      var newY = piece.y + shape[i][1];

      // 範囲外チェック
      if (newX < 0 || newX >= COLS || newY < 0 || newY >= ROWS) {
        return false;
      }

      // 他のブロックとの衝突チェック
      if (board[newY][newX] !== '') {
        return false;
      }
    }

    return true;
  }

  // ============================================
  // ゴースト位置の計算(ハードドロップ先)
  // ============================================
  function getGhostY(piece) {
    var ghostY = piece.y;

    while (true) {
      var testPiece = { type: piece.type, rotation: piece.rotation, x: piece.x, y: ghostY + 1 };
      if (!isValidPosition(testPiece)) {
        break;
      }
      ghostY++;
    }

    return ghostY;
  }

  // ============================================
  // ボードの描画更新
  // ============================================
  function render() {
    // すべてのセルをリセット
    for (var y = 0; y < ROWS; y++) {
      for (var x = 0; x < COLS; x++) {
        if (board[y][x] !== '') {
          cells[y][x].className = 'tetris-cell cell-' + board[y][x];
        } else {
          cells[y][x].className = 'tetris-cell';
        }
      }
    }

    if (currentPiece) {
      // ゴーストピースを描画
      var ghostY = getGhostY(currentPiece);
      if (ghostY !== currentPiece.y) {
        var ghostShape = getShape(currentPiece);
        for (var i = 0; i < ghostShape.length; i++) {
          var gx = currentPiece.x + ghostShape[i][0];
          var gy = ghostY + ghostShape[i][1];
          if (gy >= 0 && gy < ROWS && gx >= 0 && gx < COLS && board[gy][gx] === '') {
            cells[gy][gx].className = 'tetris-cell cell-ghost';
          }
        }
      }

      // 現在のピースを描画
      var shape = getShape(currentPiece);
      var color = TETROMINOS[currentPiece.type].color;
      for (var i = 0; i < shape.length; i++) {
        var px = currentPiece.x + shape[i][0];
        var py = currentPiece.y + shape[i][1];
        if (py >= 0 && py < ROWS && px >= 0 && px < COLS) {
          cells[py][px].className = 'tetris-cell cell-' + color;
        }
      }
    }

    // ネクストピースを描画
    renderNextPiece();

    // ホールドピースを描画
    renderHoldPiece();
  }

  // ============================================
  // ネクストピースの描画
  // ============================================
  function renderNextPiece() {
    // リセット
    for (var y = 0; y < 4; y++) {
      for (var x = 0; x < 4; x++) {
        nextCells[y][x].className = 'next-cell';
      }
    }

    if (!nextPieceType) return;

    var shape = TETROMINOS[nextPieceType].shapes[0];
    var color = TETROMINOS[nextPieceType].color;

    // ピースを中央寄せするためのオフセットを計算
    var minX = 4, maxX = 0, minY = 4, maxY = 0;
    for (var i = 0; i < shape.length; i++) {
      if (shape[i][0] < minX) minX = shape[i][0];
      if (shape[i][0] > maxX) maxX = shape[i][0];
      if (shape[i][1] < minY) minY = shape[i][1];
      if (shape[i][1] > maxY) maxY = shape[i][1];
    }
    var pieceWidth = maxX - minX + 1;
    var pieceHeight = maxY - minY + 1;
    var offsetX = Math.floor((4 - pieceWidth) / 2) - minX;
    var offsetY = Math.floor((4 - pieceHeight) / 2) - minY;

    for (var i = 0; i < shape.length; i++) {
      var nx = shape[i][0] + offsetX;
      var ny = shape[i][1] + offsetY;
      if (nx >= 0 && nx < 4 && ny >= 0 && ny < 4) {
        nextCells[ny][nx].className = 'next-cell cell-' + color;
      }
    }
  }

  // ============================================
  // ホールドピースの描画
  // ============================================
  function renderHoldPiece() {
    // リセット
    for (var y = 0; y < 4; y++) {
      for (var x = 0; x < 4; x++) {
        holdCells[y][x].className = 'hold-cell';
      }
    }

    // ホールド使用済みならボックスを暗くする
    if (holdUsed) {
      holdPieceBox.classList.add('hold-locked');
    } else {
      holdPieceBox.classList.remove('hold-locked');
    }

    if (!holdPieceType) return;

    var shape = TETROMINOS[holdPieceType].shapes[0];
    var color = TETROMINOS[holdPieceType].color;

    // ピースを中央寄せするためのオフセットを計算
    var minX = 4, maxX = 0, minY = 4, maxY = 0;
    for (var i = 0; i < shape.length; i++) {
      if (shape[i][0] < minX) minX = shape[i][0];
      if (shape[i][0] > maxX) maxX = shape[i][0];
      if (shape[i][1] < minY) minY = shape[i][1];
      if (shape[i][1] > maxY) maxY = shape[i][1];
    }
    var pieceWidth = maxX - minX + 1;
    var pieceHeight = maxY - minY + 1;
    var offsetX = Math.floor((4 - pieceWidth) / 2) - minX;
    var offsetY = Math.floor((4 - pieceHeight) / 2) - minY;

    for (var i = 0; i < shape.length; i++) {
      var hx = shape[i][0] + offsetX;
      var hy = shape[i][1] + offsetY;
      if (hx >= 0 && hx < 4 && hy >= 0 && hy < 4) {
        holdCells[hy][hx].className = 'hold-cell cell-' + color;
      }
    }
  }

  // ============================================
  // ピースをボードに固定
  // ============================================
  function lockPiece() {
    var shape = getShape(currentPiece);
    var color = TETROMINOS[currentPiece.type].color;

    for (var i = 0; i < shape.length; i++) {
      var px = currentPiece.x + shape[i][0];
      var py = currentPiece.y + shape[i][1];
      if (py >= 0 && py < ROWS && px >= 0 && px < COLS) {
        board[py][px] = color;
      }
    }

    // ロックタイマーをクリア
    clearLockTimer();
    isLocking = false;
  }

  // ============================================
  // ライン消去チェック
  // ============================================
  function clearLines() {
    var linesCleared = 0;
    var linesToClear = [];

    for (var y = ROWS - 1; y >= 0; y--) {
      var full = true;
      for (var x = 0; x < COLS; x++) {
        if (board[y][x] === '') {
          full = false;
          break;
        }
      }
      if (full) {
        linesToClear.push(y);
        linesCleared++;
      }
    }

    if (linesCleared > 0) {
      // 消去アニメーション
      for (var i = 0; i < linesToClear.length; i++) {
        var row = linesToClear[i];
        for (var x = 0; x < COLS; x++) {
          cells[row][x].classList.add('clearing');
        }
      }

      // アニメーション後にラインを実際に消去
      setTimeout(function () {
        // ラインを上から順にソート(下から消すため)
        linesToClear.sort(function (a, b) { return a - b; });

        for (var i = linesToClear.length - 1; i >= 0; i--) {
          board.splice(linesToClear[i], 1);
        }

        // 上に空行を追加
        for (var i = 0; i < linesCleared; i++) {
          var emptyRow = [];
          for (var x = 0; x < COLS; x++) {
            emptyRow.push('');
          }
          board.unshift(emptyRow);
        }

        // スコア計算
        var lineScore = LINE_SCORES[linesCleared] || (linesCleared * 200);
        score += lineScore * level;
        totalLines += linesCleared;

        // レベルアップ
        var newLevel = Math.floor(totalLines / LINES_PER_LEVEL) + 1;
        if (newLevel > level) {
          level = newLevel;
          currentSpeed = calcSpeed();
          levelEl.textContent = level;
        }

        updateScore();
        render();
      }, 300);
    }

    return linesCleared;
  }

  // ============================================
  // スコア更新
  // ============================================
  function updateScore() {
    scoreEl.textContent = score;
    if (score > bestScore) {
      bestScore = score;
      localStorage.setItem('bestTetris', bestScore.toString());
    }
    bestScoreEl.textContent = bestScore;
  }

  // ============================================
  // 速度の計算(レベルに応じて速くなる)
  // ============================================
  function calcSpeed() {
    var speed = INITIAL_SPEED - ((level - 1) * SPEED_DECREASE);
    return Math.max(speed, MIN_SPEED);
  }

  // ============================================
  // ピースを左右に移動
  // ============================================
  function movePiece(dx) {
    if (!currentPiece || !gameRunning) return;

    var testPiece = {
      type: currentPiece.type,
      rotation: currentPiece.rotation,
      x: currentPiece.x + dx,
      y: currentPiece.y
    };

    if (isValidPosition(testPiece)) {
      currentPiece.x = testPiece.x;

      // ロック中に横移動した場合、ロックタイマーをリセット
      if (isLocking) {
        resetLockTimer();
      }

      render();
    }
  }

  // ============================================
  // ピースを回転
  // ============================================
  function rotatePiece() {
    if (!currentPiece || !gameRunning) return;

    var newRotation = (currentPiece.rotation + 1) % 4;
    var testPiece = {
      type: currentPiece.type,
      rotation: newRotation,
      x: currentPiece.x,
      y: currentPiece.y
    };

    // 通常の回転を試す
    if (isValidPosition(testPiece)) {
      currentPiece.rotation = newRotation;
      if (isLocking) {
        resetLockTimer();
      }
      render();
      return;
    }

    // ウォールキック: 左右にずらして試す
    var kicks = [1, -1, 2, -2];
    for (var i = 0; i < kicks.length; i++) {
      testPiece.x = currentPiece.x + kicks[i];
      if (isValidPosition(testPiece)) {
        currentPiece.rotation = newRotation;
        currentPiece.x = testPiece.x;
        if (isLocking) {
          resetLockTimer();
        }
        render();
        return;
      }
    }
  }

  // ============================================
  // ソフトドロップ(1マス下に移動)
  // ============================================
  function softDrop() {
    if (!currentPiece || !gameRunning) return false;

    var testPiece = {
      type: currentPiece.type,
      rotation: currentPiece.rotation,
      x: currentPiece.x,
      y: currentPiece.y + 1
    };

    if (isValidPosition(testPiece)) {
      currentPiece.y = testPiece.y;
      score += 1;
      updateScore();
      render();
      return true;
    }

    return false;
  }

  // ============================================
  // ハードドロップ(一番下まで落とす)
  // ============================================
  function hardDrop() {
    if (!currentPiece || !gameRunning) return;

    var ghostY = getGhostY(currentPiece);
    var dropDistance = ghostY - currentPiece.y;
    currentPiece.y = ghostY;
    score += dropDistance * 2;
    updateScore();

    // 即座にロック
    clearLockTimer();
    isLocking = false;
    lockPiece();

    var linesCleared = clearLines();

    // 次のピースを出す
    currentPiece = spawnPiece();
    if (!currentPiece) {
      gameOver();
      return;
    }

    render();

    // ゲームループをリスタート
    clearGameLoop();
    if (linesCleared > 0) {
      // ライン消去アニメーション後にゲームループ再開
      gameLoopTimer = setTimeout(gameStep, 350);
    } else {
      gameLoopTimer = setTimeout(gameStep, currentSpeed);
    }
  }

  // ============================================
  // ホールド(ピースを保存・交換)
  // ============================================
  function holdPiece() {
    if (!currentPiece || !gameRunning || holdUsed) return;

    var currentType = currentPiece.type;

    // ロック状態をリセット
    clearLockTimer();
    isLocking = false;

    if (holdPieceType) {
      // ホールド中のピースと交換
      var swapType = holdPieceType;
      holdPieceType = currentType;

      var piece = {
        type: swapType,
        rotation: 0,
        x: Math.floor(COLS / 2) - 1,
        y: 0
      };
      if (swapType === 'I') {
        piece.x = Math.floor(COLS / 2) - 2;
      }

      if (!isValidPosition(piece)) {
        gameOver();
        return;
      }

      currentPiece = piece;
    } else {
      // 初回ホールド: 現在のピースを保存して次のピースを出す
      holdPieceType = currentType;
      currentPiece = spawnPiece();
      if (!currentPiece) {
        gameOver();
        return;
      }
    }

    // 同じピースで2回ホールドできないようにする
    holdUsed = true;

    render();

    // ゲームループをリスタート
    clearGameLoop();
    gameLoopTimer = setTimeout(gameStep, currentSpeed);
  }

  // ============================================
  // ロックタイマー管理
  // ============================================
  function clearLockTimer() {
    if (lockTimer) {
      clearTimeout(lockTimer);
      lockTimer = null;
    }
  }

  function resetLockTimer() {
    clearLockTimer();
    lockTimer = setTimeout(function () {
      if (!gameRunning || !currentPiece) return;

      // まだ接地しているか確認
      var testPiece = {
        type: currentPiece.type,
        rotation: currentPiece.rotation,
        x: currentPiece.x,
        y: currentPiece.y + 1
      };

      if (!isValidPosition(testPiece)) {
        // ロック実行
        lockPiece();
        isLocking = false;

        var linesCleared = clearLines();

        currentPiece = spawnPiece();
        if (!currentPiece) {
          gameOver();
          return;
        }

        render();

        clearGameLoop();
        if (linesCleared > 0) {
          gameLoopTimer = setTimeout(gameStep, 350);
        } else {
          gameLoopTimer = setTimeout(gameStep, currentSpeed);
        }
      } else {
        isLocking = false;
      }
    }, LOCK_DELAY);
  }

  // ============================================
  // ゲームループのクリア
  // ============================================
  function clearGameLoop() {
    if (gameLoopTimer) {
      clearTimeout(gameLoopTimer);
      gameLoopTimer = null;
    }
  }

  // ============================================
  // ゲームの1ステップ(自動落下)
  // ============================================
  function gameStep() {
    if (!gameRunning || gameOverFlag || !currentPiece) return;

    var testPiece = {
      type: currentPiece.type,
      rotation: currentPiece.rotation,
      x: currentPiece.x,
      y: currentPiece.y + 1
    };

    if (isValidPosition(testPiece)) {
      currentPiece.y = testPiece.y;
      isLocking = false;
      clearLockTimer();
      render();
      gameLoopTimer = setTimeout(gameStep, currentSpeed);
    } else {
      // 接地した。ロック猶予を開始
      if (!isLocking) {
        isLocking = true;
        resetLockTimer();
      }
      render();
      // 自動落下ループは止まる。ロックタイマーが次のステップを引き受ける
      gameLoopTimer = setTimeout(gameStep, currentSpeed);
    }
  }

  // ============================================
  // ゲームオーバー処理
  // ============================================
  function gameOver() {
    gameRunning = false;
    gameOverFlag = true;

    clearGameLoop();
    clearLockTimer();

    finalScoreEl.textContent = score;

    // ベストスコア更新チェック
    if (score >= bestScore && score > 0) {
      bestResultEl.textContent = 'ハイスコア更新!';
    } else {
      bestResultEl.textContent = 'ベスト: ' + bestScore;
    }

    // 少し遅延させてオーバーレイを表示
    setTimeout(function () {
      gameOverOverlay.classList.add('active');
      if (window.Leaderboard) Leaderboard.show('tetris', score);
    }, 300);
  }

  // ============================================
  // ゲーム開始・リスタート
  // ============================================
  function startGame() {
    // タイマーをクリア
    clearGameLoop();
    clearLockTimer();

    // 状態をリセット
    score = 0;
    level = 1;
    totalLines = 0;
    gameOverFlag = false;
    isLocking = false;
    holdPieceType = null;
    holdUsed = false;
    currentSpeed = INITIAL_SPEED;
    updateScore();
    levelEl.textContent = level;

    // オーバーレイを非表示
    gameOverOverlay.classList.remove('active');
    gameStartOverlay.classList.remove('active');

    // ボードを初期化
    initBoard();

    // 最初のピースを生成
    nextPieceType = randomPieceType();
    currentPiece = spawnPiece();
    render();

    // ゲームループ開始
    gameRunning = true;
    gameLoopTimer = setTimeout(gameStep, currentSpeed);
  }

  // ============================================
  // キーボード操作
  // ============================================
  document.addEventListener('keydown', function (e) {
    if (!gameRunning || gameOverFlag) {
      // スタート画面でキーが押されたらゲーム開始
      if (e.key === 'Enter' || e.key === ' ') {
        e.preventDefault();
        if (!gameRunning && !gameOverFlag) {
          startGame();
        }
      }
      return;
    }

    switch (e.key) {
      case 'ArrowLeft':
      case 'a':
      case 'A':
        e.preventDefault();
        movePiece(-1);
        break;
      case 'ArrowRight':
      case 'd':
      case 'D':
        e.preventDefault();
        movePiece(1);
        break;
      case 'ArrowUp':
      case 'w':
      case 'W':
        e.preventDefault();
        rotatePiece();
        break;
      case 'ArrowDown':
      case 's':
      case 'S':
        e.preventDefault();
        if (softDrop()) {
          // ソフトドロップ成功時、ゲームループをリセット
          clearGameLoop();
          gameLoopTimer = setTimeout(gameStep, currentSpeed);
        }
        break;
      case ' ':
        e.preventDefault();
        hardDrop();
        break;
      case 'Shift':
      case 'c':
      case 'C':
        e.preventDefault();
        holdPiece();
        break;
    }
  });

  // ============================================
  // モバイル操作ボタン
  // ============================================
  function setupMobileButton(btnId, action) {
    var btn = document.getElementById(btnId);
    if (!btn) return;

    btn.addEventListener('click', function (e) {
      e.preventDefault();
      if (!gameRunning && !gameOverFlag) {
        startGame();
        return;
      }
      if (gameRunning) {
        action();
      }
    });

    btn.addEventListener('touchstart', function (e) {
      e.preventDefault();
      if (!gameRunning && !gameOverFlag) {
        startGame();
        return;
      }
      if (gameRunning) {
        action();
      }
    }, { passive: false });
  }

  setupMobileButton('btn-hold', function () { holdPiece(); });
  setupMobileButton('btn-left', function () { movePiece(-1); });
  setupMobileButton('btn-right', function () { movePiece(1); });
  setupMobileButton('btn-rotate', function () { rotatePiece(); });
  setupMobileButton('btn-down', function () {
    if (softDrop()) {
      clearGameLoop();
      gameLoopTimer = setTimeout(gameStep, currentSpeed);
    }
  });
  setupMobileButton('btn-drop', function () { hardDrop(); });

  // ============================================
  // ボタンイベント
  // ============================================
  document.getElementById('btn-new-game').addEventListener('click', function () {
    startGame();
  });

  document.getElementById('btn-retry').addEventListener('click', function () {
    startGame();
  });

  document.getElementById('btn-start').addEventListener('click', function () {
    startGame();
  });

  // ============================================
  // ページ離脱時にゲームループを停止
  // ============================================
  document.addEventListener('visibilitychange', function () {
    if (document.hidden && gameRunning) {
      clearGameLoop();
      clearLockTimer();
      gameRunning = false;
    }
  });

  // ============================================
  // 初期化
  // ============================================
  bestScoreEl.textContent = bestScore;
  createBoard();
  createNextPieceDisplay();
  createHoldPieceDisplay();
  initBoard();

  // ネクストピースのプレビュー用に初期化
  nextPieceType = randomPieceType();
  renderNextPiece();
})();

このコードで学べるポイント

自分のパソコンで動かすには

上のコードをそれぞれのファイル名で同じフォルダに保存し、index.htmlをブラウザで開けばゲームが動きます。各コードブロックのファイル名バーにある「コピー」ボタンを使うと便利です。

このゲームはサイト全体で共通して使うファイル(共通CSSやヘッダー)も少し利用しているため、ソースのファイルだけで開くとヘッダーなどサイト共通の部分は表示されません。でもゲーム本体はちゃんと動くので、仕組みを学んだり改造してためすには十分です。

慣れてきたら、色を変えたりルールを少しいじったり、自由に改造してみてください。コードを「読む」次は「いじってみる」のがいちばんの上達法です。

このコードは使っていいの?

このソースコードは、プログラミングの勉強のために自由に読んだり、コピーして動かしたり、改造したりして大丈夫です。学校の授業や自由研究で参考にするのも歓迎します。ぜひ「自分だったらどう作るか」を考えるきっかけにしてください。

ただし、コードをそのまままるごとコピーして自分の作品やサービスとして公開・配布するのはご遠慮ください。あくまで学習用としてお使いください。

もっと学びたい人へ

コードを読んで「自分でも作ってみたい!」と思ったら、ブロックパズルの作り方ガイドがおすすめです。このページが「完成したコード」を見せるのに対して、作り方ガイドは何もない状態から1ステップずつ組み立てていく流れを解説しています。

ひなテックでは、こうしたゲームづくりを先生といっしょに楽しく学べる教室を開いています。「コードを読むだけじゃなく、ちゃんと作れるようになりたい」という人は、無料体験にぜひ来てみてください。