このサイトはアフィリエイトリンクを含んでいます
スポンサーリンク

WordPressサイトに“遊べる”テトリスを埋め込む理由と概要 Ver.2.0.8

TETRIS V2.0 AIで調べてみた
スポンサーリンク

ぼくがこのブラウザ版テトリスをWordPressに導入した背景は、サイト訪問者の滞在時間を伸ばし、SNSで自然に拡散される体験型コンテンツを手軽に作りたかったからです。プラグインに頼らず、HTML/CSS/JavaScriptだけで実装することで、テーマ更新や他プラグインとの干渉リスクを最小化し、純粋な学習教材としても有用なコードベースが構築できました。

この記事では、ゲーム領域と背景の切り分け、UI/UX(長押しドロップ、Nextピースのセンタリング)、コアロジック(マトリクス管理・衝突判定・ライン消去)、サウンド(BGM・効果音の分離再生)、スコア・レベル設計の最適化といった各要素を、コード例+行単位の詳細解説で丁寧に解説します。実装サンプルをコピペするだけで、あなたのWordPressサイトにも“遊べるテトリス”が完成します!

あんちゃん
あんちゃん

PCブラウザ版での操作方法はこれさ↓
上矢印キーで、右回りでブロックが回るよ。

左右の矢印キーでブロックを左右に動かせるよ!

下矢印キーはブロックが下に落ちます~。

最初に「START」ボタンを押してね~。

スマホ版はゲーム下のボタンで操作してください!

スポンサーリンク

ゲーム領域と背景の分離

WordPressテーマの背景と、テトリスを描く黒いキャンバス領域は分けて管理します。こうすることで、テーマの背景画像(宇宙っぽいメカニカルアート)を全体に表示しつつ、ゲーム画面だけを黒く塗りつぶした状態で描画できます。

/* ページ全体の背景 */
html, body {
  height: 100%;
  background: url('…/tetris_background.webp') center/cover no-repeat;
  display: flex;
  justify-content: center;
  align-items: center;
  font-family: sans-serif;
}

/* ゲームボード本体 */
#boardWrapper {
  width: 300px;
  height: 520px;
  background: #000;
  border: 2px solid #444;
  position: relative;
}
  • 背景画像background: url(...) center/cover no-repeat;で縦横比を保持しながら画面いっぱいにフィット。
  • ゲーム領域:固定ピクセルサイズを採用し、Canvas内部の座標系をJavaScriptで扱いやすく。

UI配置とタッチ/長押し操作のコツ

ゲームボード下中央に「◀」「⟳」「▶」「↓」の操作ボタンを並べ、サイドには「スコア」「START」「Nextピース枠」を配置。マウスやタッチ操作時にテキスト選択が入らないよう、次のCSSを追加します。

button {
  user-select: none;           /* 長押し時のテキスト選択を無効化 */
  -webkit-user-select: none;
  touch-action: none;          /* タッチ動作解釈の邪魔を防止 */
}

長押しでの連続落下(ソフトドロップ)

ボタンを押しっぱなしにすると、テトリス特有の「スーッ」と落ちる挙動を実現します。

let dropHold;
const dropBtn = document.getElementById('dropButton');

dropBtn.addEventListener('mousedown', () => {
  playerDrop();                 // 一度だけ即時落下
  dropHold = setInterval(playerDrop, 100);
});
dropBtn.addEventListener('mouseup', () => clearInterval(dropHold));
dropBtn.addEventListener('mouseleave', () => clearInterval(dropHold));

// モバイル対応
dropBtn.addEventListener('touchstart', e => {
  e.preventDefault();           // テキスト選択を防ぐ
  playerDrop();
  dropHold = setInterval(playerDrop, 100);
});
dropBtn.addEventListener('touchend', () => clearInterval(dropHold));
  • playerDrop() は一行分の落下&着地判定を行う関数。
  • setInterval によって 100ms 間隔で連続落下。
  • e.preventDefault() を必ず入れて、モバイルの長押しメニューを抑制。

コアロジック解説:マトリクス管理から衝突判定まで

テトリスの根幹は「盤面(マトリクス)」を操作することにあります。以下では、主要関数をピックアップして詳細解説します。

盤面生成とBagランダム

const ROWS = 20, COLS = 12;
const arena = Array.from({ length: ROWS }, () => Array(COLS).fill(0));

function createBag() {
  const types = ['T','J','L','O','S','Z','I'];
  for (let i = types.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    [types[i], types[j]] = [types[j], types[i]];
  }
  return types;
}
  • arena:20行×12列の2次元配列。初期値はすべて0
  • createBag():最新の“Tetris Guideline”に準じた7-bagシャッフルで偏りを解消。

衝突判定

function collide(A, P) {
  for (let y = 0; y < P.matrix.length; y++) {
    for (let x = 0; x < P.matrix[y].length; x++) {
      if (P.matrix[y][x]) {
        const newX = x + P.pos.x, newY = y + P.pos.y;
        if (newX < 0 || newX >= COLS || newY >= ROWS ||
           (newY >= 0 && A[newY][newX])) {
          return true;
        }
      }
    }
  }
  return false;
}
  • 配列アクセス前に**範囲外チェック**を行い、安全にアクセス。
  • 既存ブロックとの重なりを検知して真偽値を返却。

マージ(固定化)

function merge(A, P) {
  P.matrix.forEach((row, y) => {
    row.forEach((val, x) => {
      if (val && y + P.pos.y >= 0) {
        A[y + P.pos.y][x + P.pos.x] = val;
      }
    });
  });
}

着地時にテトリミノを盤面に書き込み、次のピース生成へつなげます。

ライン消去&スコア・レベル設計

1〜4行同時消去ごとに異なる得点と音を再生し、レベルアップで落下速度を加速させます。

function arenaSweep() {
  let lines = 0;
  for (let y = ROWS - 1; y >= 0; y--) {
    if (arena[y].every(v => v)) {
      arena.splice(y, 1);
      arena.unshift(new Array(COLS).fill(0));
      lines++;
      y++;
    }
  }
  if (lines) {
    switch (lines) {
      case 1: player.score += 5;   break;
      case 2: player.score += 15;  break;
      case 3: player.score += 30;  break;
      case 4: player.score += 50;  break;
    }
    player.level = Math.floor(player.score / 100) + 1;
    dropInterval = Math.max(100, 1000 - (player.level - 1) * 100);

    // 効果音
    if (lines === 4) sounds.tetris.play();
    else             sounds.clear.play();
  }
}
  • 1行→5点、2行→15点、3行→30点、4行→50点の配点。
  • レベルはscore ÷ 100の切り捨て+1。
  • 落下速度は最速100msまで加速。

描画ループとNextピースのセンタリング

CanvasをrequestAnimationFrameで更新し、Nextピースを可変サイズでも中央表示します。

function update(time = 0) {
  const delta = time - lastTime;
  lastTime = time;
  dropCounter += delta;
  if (dropCounter > dropInterval) playerDrop();
  draw();
  if (!isOver) requestAnimationFrame(update);
}

function draw() {
  ctx.clearRect(0,0,canvas.width,canvas.height);
  drawMatrix(arena, 0, 0, ctx);
  drawMatrix(player.matrix, player.pos.x * TILE, player.pos.y * TILE, ctx);

  // Nextピース中央表示
  nextCtx.clearRect(0,0,nextCanvas.width,nextCanvas.height);
  const nw = player.next[0].length, nh = player.next.length;
  const ox = Math.floor((nextCanvas.width  - nw * TILE) / 2);
  const oy = Math.floor((nextCanvas.height - nh * TILE) / 2);
  drawMatrix(player.next, ox, oy, nextCtx);
}

まとめ:WordPressへの埋め込み手順

  1. テーマフォルダ内に /tetris/ を作成し、HTML/CSS/JS/音声ファイルを配置。
  2. 固定ページや投稿のHTMLモードで以下を貼り付け: <iframe src="<?php echo get_template_directory_uri(); ?>/tetris/index.html?v=2.0.8" width="700" height="600" frameborder="0"></iframe>
  3. キャッシュ対策にバージョンパラメータ(?v=2.0.8)を付与。
  4. 必要に応じてCSSでmax-width:100%; height:auto;を追加。

即時実行関数(IIFE)によるスコープ管理:
コード全体を (() => { … })(); で囲むことで、グローバル変数の汚染を防ぎ、複数ページへの埋め込み時にも他スクリプトと衝突しにくくなっています。

定数定義とキャンバス初期化:
const COLS=12, ROWS=20, TILE=20; で盤面サイズを一元管理。
canvas.width=COLS*TILE;canvas.height=ROWS*TILE; でピクセル単位を厳密に計算し、描画がズレないようにしています。

定数役割
COLS横セル数(12)
ROWS縦セル数(20)
TILE1セルのピクセル幅(20px)
MIN_INTERVAL落下速度の最小間隔(100ms)

マトリクス&Bag乱数:
arena は 2 次元配列で盤面を管理。
createBag() は 7 種のテトリミノを Fisher–Yates shuffle で混ぜ、偏りなく次のピースを生成します。

衝突判定の詳細:
次のように4方向と範囲外をチェックしてブロックの重なりを検出します。

if (x+pos.x < 0 || x+pos.x >= COLS ||
    y+pos.y >= ROWS ||
    (y+pos.y >= 0 && arena[y+pos.y][x+pos.x])) {
  return true;
}

範囲外チェックを最初に行うことで無駄な配列アクセスを抑制し、高速化しています。

ライン消去と得点計算:
消した行数に応じて得点と効果音を切り替え:

  • 1行:+5点
  • 2行:+15点
  • 3行:+30点
  • 4行(テトリス):+50点

消去後は dropInterval = Math.max(100, 1000 - (level-1)*100); でレベルに応じて落下速度を調整します。

描画ループとNextピース:
requestAnimationFrame(update) で 1 フレームずつ時間差を計測し、dropCounter が閾値を超えたら自動落下。
Next ピースはキャンバス幅からブロックサイズを引き、Math.floor で中央にオフセットを計算しています。

操作ボタンのイベントバインド:
矢印キーやボタン押下で playerMoveplayerDropplayer.rotate を実行。
長押し時は setInterval で 100ms ごとに連続落下させています。

サウンドの非同期再生:
各効果音は再生前に currentTime=0 を設定し、重なっても必ず最初から再生されるようにしています。BGM はループ再生し、ゲームオーバー時に pause() で止めます。

これらの仕組みを踏まえれば、コードの各ブロックが何のためにあり、どのように動いているかが明確になります。カスタマイズや機能追加の際にも、迷わず手を入れられる基盤が整ったと言えるでしょう。

あんちゃん
あんちゃん

では、このテトリスのHTML、CSS、Javascriptのコードを公開しますね!

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Browser Tetris v2.0.8</title>
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }

    html,
    body {
      height: 100%;
      background-image: url('https://tokodomo.xyz/wp-content/uploads/2025/06/background_v2.0.8.jpg');
      background-size: cover;
      background-position: center;
      display: flex;
      justify-content: center;
      align-items: center;
      font-family: sans-serif;
    }

    #mainWrapper {
      display: flex;
      gap: 20px;
    }

    #boardColumn {
      display: flex;
      flex-direction: column;
      align-items: center;
    }

    #boardWrapper {
      position: relative;
      width: 300px;
      height: 520px;
      background: #000;
      border: 2px solid #444;
    }

    #game {
      width: 100%;
      height: 100%;
      display: block;
    }

    #finalScore {
      position: absolute;
      top: 200px;
      left: 0;
      width: 100%;
      text-align: center;
      background: rgba(255, 255, 255, 0.8);
      color: #000;
      font-size: 1.5rem;
      line-height: 2rem;
      display: none;
      user-select: none;
    }

    #boardControls {
      display: flex;
      gap: 8px;
      margin-top: 8px;
    }

    #sidePanel {
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 15px;
    }

    #scoreContainer {
      font-size: 1.2rem;
      color: #fff;
    }

    #nextPieceContainer {
      text-align: center;
    }

    #nextPieceContainer canvas {
      background: #000;
      border: 1px solid #444;
      width: 80px;
      height: 80px;
      display: block;
      margin-bottom: 4px;
    }

    button {
      padding: 8px 12px;
      font-size: 1rem;
      border: none;
      border-radius: 4px;
      background: #444;
      color: #fff;
      cursor: pointer;
      user-select: none;
      -webkit-user-select: none;
      touch-action: none;
    }
  </style>
</head>
<body>
  <div id="mainWrapper">
    <div id="boardColumn">
      <div id="boardWrapper">
        <canvas id="game" tabindex="0"></canvas>
        <div id="finalScore">GAME OVER</div>
      </div>
      <div id="boardControls">
        <button id="leftButton"></button>
        <button id="rotateButton"></button>
        <button id="rightButton"></button>
        <button id="dropButton"></button>
      </div>
    </div>
    <div id="sidePanel">
      <div id="scoreContainer">Score: 0 | Level: 1</div>
      <button id="startButton">START</button>
      <div id="nextPieceContainer">
        <canvas id="nextPiece"></canvas>
        <div>Next</div>
      </div>
    </div>
  </div>

  <audio id="bgm" loop src="https://tokodomo.xyz/wp-content/uploads/2024/09/Fall-of-the-Blocks.mp3"></audio>
  <audio id="dropSound" src="https://tokodomo.xyz/wp-content/uploads/2024/09/drop.mp3"></audio>
  <audio id="rotateSound" src="https://tokodomo.xyz/wp-content/uploads/2024/09/rotate.mp3"></audio>
  <audio id="clearSound" src="https://tokodomo.xyz/wp-content/uploads/2024/09/line-clear.mp3"></audio>
  <audio id="tetrisSound" src="https://tokodomo.xyz/wp-content/uploads/2024/09/tetris.mp3"></audio>

  <script>
    (() => {
      const COLS = 12;
      const ROWS = 20;
      const TILE = 20;
      const MIN_INTERVAL = 100;

      const canvas = document.getElementById('game');
      const ctx = canvas.getContext('2d');
      const nextCanvas = document.getElementById('nextPiece');
      const nextCtx = nextCanvas.getContext('2d');
      const scoreEl = document.getElementById('scoreContainer');
      const finalEl = document.getElementById('finalScore');
      const startBtn = document.getElementById('startButton');
      const controls = {
        left: document.getElementById('leftButton'),
        rotate: document.getElementById('rotateButton'),
        right: document.getElementById('rightButton'),
        drop: document.getElementById('dropButton')
      };
      const sounds = {
        bgm: document.getElementById('bgm'),
        drop: document.getElementById('dropSound'),
        rotate: document.getElementById('rotateSound'),
        clear: document.getElementById('clearSound'),
        tetris: document.getElementById('tetrisSound')
      };

      canvas.width = COLS * TILE;
      canvas.height = ROWS * TILE;
      nextCanvas.width = 4 * TILE;
      nextCanvas.height = 4 * TILE;

      const arena = Array.from({ length: ROWS }, () => new Array(COLS).fill(0));

      function createBag() {
        const p = ['T', 'J', 'L', 'O', 'S', 'Z', 'I'];
        for (let i = p.length - 1; i > 0; i--) {
          const j = Math.floor(Math.random() * (i + 1));
          [p[i], p[j]] = [p[j], p[i]];
        }
        return p;
      }

      class Player {
        constructor() {
          this.reset();
          this.score = 0;
          this.level = 1;
        }

        reset() {
          if (!this.bag || !this.bag.length) this.bag = createBag();
          this.matrix = this.next || createPiece(this.bag.pop());
          this.next = createPiece(this.bag.pop());
          this.pos = {
            x: Math.floor((COLS - this.matrix[0].length) / 2),
            y: 0
          };
          if (collide(arena, this)) endGame();
        }

        rotate(dir) {
          const px = this.pos.x;
          rotateMatrix(this.matrix, dir);
          let off = 1;
          while (collide(arena, this)) {
            this.pos.x += off;
            off = -(off + (off > 0 ? 1 : -1));
            if (off > this.matrix[0].length) {
              rotateMatrix(this.matrix, -dir);
              this.pos.x = px;
              return;
            }
          }
          sounds.rotate.currentTime = 0;
          sounds.rotate.play();
        }
      }

      function createPiece(type) {
        switch (type) {
          case 'T': return [[0,0,0],[1,1,1],[0,1,0]];
          case 'O': return [[2,2],[2,2]];
          case 'L': return [[0,3,0],[0,3,0],[0,3,3]];
          case 'J': return [[0,4,0],[0,4,0],[4,4,0]];
          case 'I': return [[0,5,0,0],[0,5,0,0],[0,5,0,0],[0,5,0,0]];
          case 'S': return [[0,6,6],[6,6,0],[0,0,0]];
          case 'Z': return [[7,7,0],[0,7,7],[0,0,0]];
        }
      }

      function rotateMatrix(m, dir) {
        for (let y = 0; y < m.length; y++) {
          for (let x = 0; x < y; x++) {
            [m[x][y], m[y][x]] = [m[y][x], m[x][y]];
          }
        }
        dir > 0 ? m.forEach(r => r.reverse()) : m.reverse();
      }

      function collide(A, P) {
        for (let y = 0; y < P.matrix.length; y++) {
          for (let x = 0; x < P.matrix[y].length; x++) {
            if (P.matrix[y][x]) {
              const newX = x + P.pos.x;
              const newY = y + P.pos.y;
              if (
                newX < 0 || newX >= COLS ||
                newY >= ROWS ||
                (newY >= 0 && A[newY][newX])
              ) {
                return true;
              }
            }
          }
        }
        return false;
      }

      function merge(A, P) {
        P.matrix.forEach((row, y) => {
          row.forEach((v, x) => {
            if (v && y + P.pos.y >= 0) {
              A[y + P.pos.y][x + P.pos.x] = v;
            }
          });
        });
      }

      function arenaSweep() {
        let lines = 0;
        for (let y = ROWS - 1; y >= 0; y--) {
          if (arena[y].every(v => v)) {
            arena.splice(y, 1);
            arena.unshift(new Array(COLS).fill(0));
            lines++;
            y++;
          }
        }
        if (lines) {
          switch (lines) {
            case 1: player.score += 5;   break;
            case 2: player.score += 15;  break;
            case 3: player.score += 30;  break;
            case 4: player.score += 50;  break;
          }
          player.level = Math.floor(player.score / 100) + 1;
          dropInterval = Math.max(
            MIN_INTERVAL,
            1000 - (player.level - 1) * 100
          );

          if (lines === 4) {
            sounds.tetris.currentTime = 0;
            sounds.tetris.play();
          } else {
            sounds.clear.currentTime = 0;
            sounds.clear.play();
          }
        }
      }

      function drawMatrix(matrix, ox, oy, context) {
        matrix.forEach((row, y) => {
          row.forEach((v, x) => {
            if (v) {
              context.fillStyle = colors[v];
              context.fillRect(
                ox + x * TILE,
                oy + y * TILE,
                TILE - 1,
                TILE - 1
              );
            }
          });
        });
      }

      function draw() {
        ctx.clearRect(0, 0, canvas.width, canvas.height);
        drawMatrix(arena, 0, 0, ctx);
        drawMatrix(
          player.matrix,
          player.pos.x * TILE,
          player.pos.y * TILE,
          ctx
        );

        nextCtx.clearRect(0, 0, nextCanvas.width, nextCanvas.height);
        const npW = player.next[0].length;
        const npH = player.next.length;
        const offX = Math.floor((
          nextCanvas.width - npW * TILE
        ) / 2);
        const offY = Math.floor((
          nextCanvas.height - npH * TILE
        ) / 2);
        drawMatrix(player.next, offX, offY, nextCtx);
      }

      function updateScore() {
        scoreEl.textContent = `Score: ${player.score} | Level: ${player.level}`;
      }

      let dropCounter = 0;
      let dropInterval = 1000;
      let lastTime = 0;
      let isOver = false;

      function update(time = 0) {
        const delta = time - lastTime;
        lastTime = time;
        dropCounter += delta;

        if (dropCounter > dropInterval) playerDrop();
        draw();

        if (!isOver) requestAnimationFrame(update);
      }

      function playerDrop() {
        player.pos.y++;
        if (collide(arena, player)) {
          player.pos.y--;
          merge(arena, player);
          sounds.drop.currentTime = 0;
          sounds.drop.play();
          arenaSweep();
          player.reset();
          updateScore();
        }
        dropCounter = 0;
      }

      function playerMove(dir) {
        player.pos.x += dir;
        if (collide(arena, player)) player.pos.x -= dir;
      }

      function endGame() {
        isOver = true;
        finalEl.style.display = 'block';
        sounds.bgm.pause();
      }

      const colors = [
        null,
        '#FF0D72',
        '#0DC2FF',
        '#0DFF72',
        '#F538FF',
        '#FF8E0D',
        '#FFE138',
        '#3877FF'
      ];

      const player = new Player();

      // キーボード操作: 矢印キーのスクロール抑制
      window.addEventListener(
        'keydown',
        e => {
          const keys = [
            'ArrowLeft',
            'ArrowRight',
            'ArrowDown',
            'ArrowUp'
          ];
          if (keys.includes(e.key)) {
            e.preventDefault();
            e.stopPropagation();
          }
          if (e.key === 'ArrowLeft') playerMove(-1);
          if (e.key === 'ArrowRight') playerMove(1);
          if (e.key === 'ArrowDown') playerDrop();
          if (e.key === 'ArrowUp') player.rotate(1);
        },
        { passive: false }
      );

      // ボタン操作
      controls.left.addEventListener('mousedown', () => playerMove(-1));
      controls.right.addEventListener('mousedown', () => playerMove(1));

      let dropHold;
      const startHold = () => {
        playerDrop();
        dropHold = setInterval(playerDrop, 100);
      };
      const stopHold = () => clearInterval(dropHold);

      ['mousedown', 'touchstart'].forEach(evt =>
        controls.drop.addEventListener(evt, e => {
          e.preventDefault();
          startHold();
        })
      );
      ['mouseup', 'mouseleave', 'touchend', 'touchcancel'].forEach(evt =>
        controls.drop.addEventListener(evt, stopHold)
      );
      controls.rotate.addEventListener('mousedown', () => player.rotate(1));

      // ゲーム開始
      startBtn.addEventListener('click', () => {
        arena.forEach(row => row.fill(0));
        isOver = false;
        finalEl.style.display = 'none';
        player.score = 0;
        player.level = 1;
        player.bag = [];
        player.next = null;
        player.reset();
        updateScore();
        dropCounter = 0;
        lastTime = 0;
        dropInterval = 1000;
        sounds.bgm.currentTime = 0;
        sounds.bgm.volume = 0.3;
        sounds.bgm.play().catch(() => {});
        canvas.focus();
        requestAnimationFrame(update);
      });
    })();
  </script>
</body>
</html>
## テトリス v2.0.8 完成コード 解説

以下は、先ほど貼り付けた **Browser Tetris v2.0.8** のコードを、Markdown 記法でパーツごとに分けて解説したものです。WordPress の投稿エディタにそのまま貼り付けてご利用ください。

---

## 1. HTML 構造

```html
<div id="mainWrapper">
  <div id="boardColumn">
    <div id="boardWrapper">
      <canvas id="game" tabindex="0"></canvas>
      <div id="finalScore">GAME OVER</div>
    </div>
    <div id="boardControls">
      <button id="leftButton">◀</button>
      <button id="rotateButton">⟳</button>
      <button id="rightButton">▶</button>
      <button id="dropButton">↓</button>
    </div>
  </div>
  <div id="sidePanel">
    <div id="scoreContainer">Score: 0 | Level: 1</div>
    <button id="startButton">START</button>
    <div id="nextPieceContainer">
      <canvas id="nextPiece"></canvas>
      <div>Next</div>
    </div>
  </div>
</div>

<audio id="bgm" loop src="…/Fall-of-the-Blocks.mp3"></audio>
<audio id="dropSound"    src="…/drop.mp3"></audio>
<audio id="rotateSound"  src="…/rotate.mp3"></audio>
<audio id="clearSound"   src="…/line-clear.mp3"></audio>
<audio id="tetrisSound"  src="…/tetris.mp3"></audio>
  • #mainWrapper:ゲーム本体とサイドパネルを左右並びにする Flex コンテナ
  • #boardWrapper:黒背景キャンバス(300×520px)を囲む枠。position: relative で「GAME OVER」表示を重ねる
  • <canvas id="game">:メイン描画領域。tabindex="0" でキーボードフォーカスを受け付ける
  • #finalScore:ゲームオーバー時に半透明白帯で文字を表示
  • #boardControls:矢印ボタン4つを並べる領域
  • #sidePanel:スコア、START ボタン、Next ピース表示の縦並び領域
  • <audio> 要素:BGM と各効果音を個別の ID で管理

2. CSS 解説

html, body {
  height: 100%;
  background-image: url('…/background_v2.0.8.jpg');
  background-size: cover;
  background-position: center;
  display: flex;
  justify-content: center;
  align-items: center;
  font-family: sans-serif;
}

#boardWrapper {
  width: 300px;
  height: 520px;
  background: #000;
  border: 2px solid #444;
  position: relative;
}

#finalScore {
  position: absolute;
  top: 200px;
  left: 0;
  width: 100%;
  text-align: center;
  background: rgba(255,255,255,0.8);
  color: #000;
  font-size: 1.5rem;
  line-height: 2rem;
  display: none;
  user-select: none;
}

button {
  padding: 8px 12px;
  font-size: 1rem;
  border: none;
  border-radius: 4px;
  background: #444;
  color: #fff;
  cursor: pointer;
  user-select: none;
  -webkit-user-select: none;
  touch-action: none;
}
  • 全画面背景background-size: cover で縦横比を維持しながら画面いっぱいに
  • ボタン本体touch-action: none でモバイルの余計なスクロール/長押し挙動を防止
  • #finalScore:見やすい半透明帯とし、display: none で初期非表示

3. JavaScript:定数とキャンバス初期化

const COLS = 12;
const ROWS = 20;
const TILE = 20;
const MIN_INTERVAL = 100;

canvas.width  = COLS * TILE;
canvas.height = ROWS * TILE;
nextCanvas.width  = 4 * TILE;
nextCanvas.height = 4 * TILE;
  • COLS, ROWS, TILE:盤面セル数と1セルのピクセル幅を集中管理
  • MIN_INTERVAL:ソフトドロップ最速間隔を定義(100ms)
  • キャンバス設定:「セル数 × タイルサイズ」で正確にピクセル指定

4. マトリクス管理&7-bag シャッフル

const arena = Array.from({ length: ROWS }, () => new Array(COLS).fill(0));

function createBag() {
  const p = ['T','J','L','O','S','Z','I'];
  for (let i = p.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    [p[i], p[j]] = [p[j], p[i]];
  }
  return p;
}
  • arena:空き/ブロックを 0/1-7 の数値で管理する 2 次元配列
  • createBag():偏りなく 7 種をランダムに並び替える Fisher–Yates アルゴリズム

5. 衝突判定 & マージ

function collide(A, P) {
  for (let y = 0; y < P.matrix.length; y++) {
    for (let x = 0; x < P.matrix[y].length; x++) {
      if (P.matrix[y][x]) {
        const newX = x + P.pos.x,
              newY = y + P.pos.y;
        if (newX < 0 || newX >= COLS ||
            newY >= ROWS ||
           (newY >= 0 && A[newY][newX])) {
          return true;
        }
      }
    }
  }
  return false;
}

function merge(A, P) {
  P.matrix.forEach((row, y) =>
    row.forEach((v, x) => {
      if (v && y + P.pos.y >= 0) {
        A[y + P.pos.y][x + P.pos.x] = v;
      }
    })
  );
}
  • collide():次フレームでの位置が「盤外」/「既存ブロック重なり」か判定
  • merge():着地したテトリミノを arena に固定化

6. ライン消去・得点・レベル調整

function arenaSweep() {
  let lines = 0;
  for (let y = ROWS - 1; y >= 0; y--) {
    if (arena[y].every(v => v)) {
      arena.splice(y, 1);
      arena.unshift(new Array(COLS).fill(0));
      lines++; y++;
    }
  }
  if (lines) {
    switch (lines) {
      case 1: player.score +=  5; break;
      case 2: player.score += 15; break;
      case 3: player.score += 30; break;
      case 4: player.score += 50; break;
    }
    player.level    = Math.floor(player.score / 100) + 1;
    dropInterval    = Math.max(MIN_INTERVAL, 1000 - (player.level - 1) * 100);
    (lines === 4 ? sounds.tetris : sounds.clear).play();
  }
}
消去行数得点
1 行5 点
2 行15 点
3 行30 点
4 行50 点
  • レベル計算:100点ごとに +1
  • ドロップ速度:最速 100ms まで加速
  • 効果音:4行なら tetris.mp3、それ以外は line-clear.mp3

7. 描画ループ & Next ピース

function update(time = 0) {
  const delta = time - lastTime;
  dropCounter += delta;
  lastTime = time;

  if (dropCounter > dropInterval) playerDrop();
  draw();
  if (!isOver) requestAnimationFrame(update);
}

function draw() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  drawMatrix(arena, 0, 0, ctx);
  drawMatrix(player.matrix, player.pos.x * TILE, player.pos.y * TILE, ctx);

  nextCtx.clearRect(0, 0, nextCanvas.width, nextCanvas.height);
  const w = player.next[0].length, h = player.next.length;
  const offX = Math.floor((nextCanvas.width  - w * TILE) / 2);
  const offY = Math.floor((nextCanvas.height - h * TILE) / 2);
  drawMatrix(player.next, offX, offY, nextCtx);
}
  • requestAnimationFrame:ブラウザ同期でスムーズ描画
  • Nextピース:行列サイズから中央オフセットを計算して描画

8. キーボード&タッチ操作

// 矢印キー操作 + スクロール抑止
window.addEventListener('keydown', e => {
  const keys = ['ArrowLeft','ArrowRight','ArrowDown','ArrowUp'];
  if (keys.includes(e.key)) {
    e.preventDefault();
    e.stopPropagation();
  }
  if (e.key === 'ArrowLeft')  playerMove(-1);
  if (e.key === 'ArrowRight') playerMove(1);
  if (e.key === 'ArrowDown')  playerDrop();
  if (e.key === 'ArrowUp')    player.rotate(1);
}, { passive: false });

// ボタン長押しでソフトドロップ
controls.drop.addEventListener('mousedown', startHold);
controls.drop.addEventListener('touchstart', e => { e.preventDefault(); startHold(); });
controls.drop.addEventListener('mouseup',   stopHold);
controls.drop.addEventListener('mouseleave',stopHold);
controls.drop.addEventListener('touchend',  stopHold);
  • preventDefault + stopPropagation:親ページのスクロールを完全にブロック
  • passive: false:スクロールキャンセルを確実に
  • touchstart + mousedown:マウス/タッチの両対応

まとめ

  • HTML:キャンバスと操作ボタンを分離して配置
  • CSS:全画面背景 + モバイル操作抑制
  • JS:マトリクス管理、衝突判定、得点・レベル設計、描画ループ
  • UX:ソフトドロップ、Nextピースセンタリング、GAME OVER 表示
  • キーボード:スクロール抑止で快適プレイ

この解説を参考に、WordPress 投稿に直接 Markdown 形式で貼り付けてみてください。さあ、自分だけのテトリスサイトを完成させましょう!

コメント

タイトルとURLをコピーしました