見出し画像

テトリスを作る - 5 行のクリア

下に積み重なっていき、一列が全て埋まった場合に行全体を消していく動作を作っていきます。


ステップ 8: 行のクリア

ブロックが一段積み上がり、横一列が埋まったときに、その行を消去してスコアを加算するロジックです。これは、ブロックがボードに固定された直後(mergePieceToBoardの直後)に実行する必要があります。

行のクリア判定と処理関数 checkRowClear の作成

この関数は、ボードの下から上に向かって行をチェックし、すべてブロックで埋まっている行を削除し、新しい空白行を上部に追加します。

// スコアを保持する変数
let score = 0; 
// 1行クリアあたりの基本スコア (適当な値)
const POINTS_PER_LINE = 10; 

function checkRowClear() {
    let linesCleared = 0; // 今回クリアした行数

    // ボードの最も下 (BOARD_HEIGHT - 1) から上 (0) に向かってチェックする
    outer: for (let y = BOARD_HEIGHT - 1; y >= 0; y--) {
        // 現在の行 (board[y]) の各マスをチェック
        for (let x = 0; x < BOARD_WIDTH; x++) {
            // 1つでも空白マス (0) があれば、この行はクリアされていない
            if (board[y][x] === 0) {
                continue outer; // 次の行のチェックへ移動
            }
        }

        // ここに到達した場合、その行 (y) は完全にブロックで埋まっている (クリア対象)

        // 1. 行を削除する
        // board.splice(y, 1) で y行目から1行分を削除
        board.splice(y, 1);
        
        // 2. 削除した分の新しい空白行をボードの最上部に追加する
        // Array(BOARD_WIDTH).fill(0) で、幅分の空白行を作成
        board.unshift(new Array(BOARD_WIDTH).fill(0)); 
        
        // 3. 削除処理をしたので、yのループを再度実行するため y をインクリメント (重要!)
        // spliceで配列の要素数が減り、yの位置が変わったため
        y++; 
        linesCleared++;
    }

    // スコア計算
    if (linesCleared > 0) {
        // 例: 複数行同時クリアはボーナス
        score += POINTS_PER_LINE * linesCleared * linesCleared; 
        console.log(`クリア: ${linesCleared}行! スコア: ${score}`);
        // 画面のスコア表示を更新する関数を後で追加
    }
}

各行(縦方向)の横方向の座標に"0"が入っているかを調べる。入っていれば埋まっていないということなのでその行は飛ばします。

2つのfor文で縦横の数字を確認します。

  outer: for (let y = BOARD_HEIGHT - 1; y >= 0; y--) {  ・・・ 縦方向
    for (let x = 0; x < BOARD_WIDTH; x++) {     ・・・ 横方向
                   if (board[y][x] === 0) {
                      continue outer; // 次の行のチェックへ移動
                   }
               }
  }

"0"以外が入ってる場合はそこに固定されたミノがあることになり消去対象になります。そして消去の方法は、splice()を使って。

board.splice(y, 1);

配列の一つを期してしまいます。そうすると一行減るので、数を合わせるために"0"を入れたものを一番上に行を追加します。

 board.unshift(new Array(BOARD_WIDTH).fill(0)); 

そして全体のy座標を変更します。これで一行減り、全体を下方向に移動します。

y++; 


スコア表示要素の準備(HTML)

スコアを表示するためのHTML要素を準備し、CSSで配置しましょう。index.html の <body> タグ内の <canvas> の近くに以下を追加します。

<body>
    <div id="game-container">
        <div id="score-panel" style="margin-left: 20px;">
            <h2>SCORE: <span id="score-value">0</span></h2>
        </div>
        
        <canvas id="tetris-canvas"></canvas>
    </div>
    
    <style>
        /* 画面中央寄せのCSSを修正 */
        body { /* ... 省略 ... */ }
        #game-container {
            display: flex; /* canvasとscore-panelを横並びにする */
            align-items: flex-start; /* 上端を揃える */
        }
    </style>
    
    <script src="tetris.js"></script>
</body>


スコア表示の更新とクリア関数の組み込み

tetris.jsでスコア表示を更新する関数を作成し、これまでの処理に組み込みます。

A. スコア更新関数の作成:

function updateScoreDisplay() {
    document.getElementById('score-value').innerText = score;
}

B. pieceDrop 関数への組み込み:

ステップ6で作成した pieceDrop 関数内で、ブロックが着地した後の処理として呼び出します。

function pieceDrop() {
    // ... (衝突判定のロジックは省略)
    if (checkCollision(currentPiece, 0, 1)) {
        // 衝突する場合 (着地)
        
        mergePieceToBoard(currentPiece); 
        
        // ⭐ 【NEW】 行のクリア判定を実行し、スコアを更新
        checkRowClear(); 
        updateScoreDisplay(); 
        
        currentPiece = createPiece();
        // ... (ゲームオーバー判定は省略)
    } else {
        // ... (落下処理は省略)
    }
}

C. hardDrop 関数への組み込み:

同様に、ステップ7で作成した hardDrop 関数内にも組み込みます。

function hardDrop() {
    // ... (落下させる while ループは省略)
    
    mergePieceToBoard(currentPiece);
    
    // ⭐ 【NEW】 行のクリア判定を実行し、スコアを更新
    checkRowClear(); 
    updateScoreDisplay(); 
    
    currentPiece = createPiece();
}

これで、ブロックが着地した際に揃った行が消去され、スコアが加算されるようになりました。

これまでの全体コード

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>JavaScript Tetris</title>
    
    <style>
        /* CSSは次のセクションで定義します */
        body {
            display: flex;
            justify-content: center;
            align-items: center;
            height: 100vh;
            margin: 0;
            background-color: #f0f0f0;
        }
        #tetris-canvas {
            border: 5px solid #333; /* 枠線をつけてゲームボードを分かりやすくします */
        }

         #game-container {
            display: flex; /* canvasとscore-panelを横並びにする */
            align-items: flex-start; /* 上端を揃える */
        }
    </style>

</head>
<body>
    <!-- <canvas id="tetris-canvas"></canvas> -->


     <div id="game-container">
        <div id="score-panel" style="margin-left: 20px;">
            <h2>SCORE: <span id="score-value">0</span></h2>
        </div>
        
        <canvas id="tetris-canvas"></canvas>
    </div>
    
    <script>    
    // ブロック1つあたりのピクセルサイズ
const BLOCK_SIZE = 30;

// テトリス盤面のサイズ(縦20マス、横10マスが一般的)
const BOARD_WIDTH = 10;
const BOARD_HEIGHT = 20;

// キャンバスのサイズを決定
const CANVAS_WIDTH = BLOCK_SIZE * BOARD_WIDTH;
const CANVAS_HEIGHT = BLOCK_SIZE * BOARD_HEIGHT;

// キャンバスを取得
const canvas = document.getElementById('tetris-canvas');
const ctx = canvas.getContext('2d');

// キャンバスにサイズを適用
canvas.width = CANVAS_WIDTH;
canvas.height = CANVAS_HEIGHT;

// ゲームボードを2次元配列で作成し、すべて0(空白)で初期化
let board = [];
for (let y = 0; y < BOARD_HEIGHT; y++) {
    // 1行につき BOARD_WIDTH の要素を持つ配列を作成
    board[y] = new Array(BOARD_WIDTH).fill(0); 
}


function drawBoard() {
    for (let y = 0; y < BOARD_HEIGHT; y++) {
        for (let x = 0; x < BOARD_WIDTH; x++) {
            if (board[y][x] !== 0) {
                // ボード上のマスが空白(0)でなければ描画
                ctx.fillStyle = 'blue'; // 仮の色
                ctx.fillRect(x * BLOCK_SIZE, y * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE);
                
                // ブロックの境界線を描画
                ctx.strokeStyle = '#000'; 
                ctx.strokeRect(x * BLOCK_SIZE, y * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE);
            }
        }
    }
}

// テトリミノの形状と色を定義する配列
// 1以上の数字はブロックの種類(色)を示す
const TETROMINOS = [
    // 0: I型 (Cyan)
    { 
        matrix: [
            [0, 0, 0, 0],
            [1, 1, 1, 1],
            [0, 0, 0, 0],
            [0, 0, 0, 0],
        ],
        color: '#00FFFF' // シアン
    },
    // 1: J型 (Blue)
    { 
        matrix: [
            [2, 0, 0],
            [2, 2, 2],
            [0, 0, 0],
        ],
        color: '#0000FF' // 青
    },
    // 2: L型 (Orange)
    { 
        matrix: [
            [0, 0, 3],
            [3, 3, 3],
            [0, 0, 0],
        ],
        color: '#FFA500' // オレンジ
    },
    // 3: O型 (Yellow)
    { 
        matrix: [
            [4, 4],
            [4, 4],
        ],
        color: '#FFFF00' // 黄色
    },
    // 4: S型 (Green)
    { 
        matrix: [
            [0, 5, 5],
            [5, 5, 0],
            [0, 0, 0],
        ],
        color: '#00FF00' // 緑
    },
    // 5: T型 (Purple)
    { 
        matrix: [
            [0, 6, 0],
            [6, 6, 6],
            [0, 0, 0],
        ],
        color: '#800080' // 紫
    },
    // 6: Z型 (Red)
    { 
        matrix: [
            [7, 7, 0],
            [0, 7, 7],
            [0, 0, 0],
        ],
        color: '#FF0000' // 赤
    }
];

// 現在操作中のテトリミノを保持するオブジェクト
let currentPiece = null; 

function createPiece() {
    // 0からTETROMINOSの数-1までの乱数を生成
    const randIndex = Math.floor(Math.random() * TETROMINOS.length);
    const pieceData = TETROMINOS[randIndex];
    
    // テトリミノの情報をオブジェクトとして返す
    return {
        matrix: pieceData.matrix,
        color: pieceData.color,
        // 開始位置 (ボードの上端中央)
        pos: { x: Math.floor(BOARD_WIDTH / 2) - Math.floor(pieceData.matrix[0].length / 2), y: 0 }
    };
}

function drawPiece(piece) {
    // テトリミノのブロックを描画
    piece.matrix.forEach((row, y) => {
        row.forEach((value, x) => {
            if (value !== 0) {
                // ボード上の座標に変換
                const drawX = (piece.pos.x + x) * BLOCK_SIZE;
                const drawY = (piece.pos.y + y) * BLOCK_SIZE;
                
                ctx.fillStyle = piece.color;
                ctx.fillRect(drawX, drawY, BLOCK_SIZE, BLOCK_SIZE);
                
                // ブロックの境界線を描画
                ctx.strokeStyle = '#000'; 
                ctx.strokeRect(drawX, drawY, BLOCK_SIZE, BLOCK_SIZE);
            }
        });
    });
}

function gameLoop() {

    // 画面のクリア
    ctx.clearRect(0, 0, canvas.width, canvas.height); 
   
    pieceDrop()
    
    // 固定されたブロックと落下中のブロックを描画   
    if (currentPiece) {
        drawPiece(currentPiece);
    }
    drawBoard();
}


/**
 * 指定された位置 (offset) にテトリミノを移動または回転させたときに、
 * ボードの境界や固定されたブロックと衝突するかどうかを判定する。
 * @param {object} piece - 現在のテトリミノオブジェクト (matrix, pos)
 * @param {number} offsetX - X軸方向の移動量 (通常 -1, 0, 1)
 * @param {number} offsetY - Y軸方向の移動量 (通常 0, 1)
 * @returns {boolean} 衝突する場合は true、しない場合は false
 */
function checkCollision(piece, offsetX, offsetY) {
    const matrix = piece.matrix;
    const currentX = piece.pos.x;
    const currentY = piece.pos.y;

    for (let y = 0; y < matrix.length; y++) {
        for (let x = 0; x < matrix[y].length; x++) {
            // ブロックのマス(値が0ではない)のみをチェック
            if (matrix[y][x] !== 0) {
                // 移動後のボード上の座標を計算
                const nextX = currentX + x + offsetX;
                const nextY = currentY + y + offsetY;

                // 衝突条件 1: ボードの左右の壁、または底を超えてしまう
                if (nextX < 0 || nextX >= BOARD_WIDTH || nextY >= BOARD_HEIGHT) {
                    return true; // 衝突 (壁または底)
                }
                
                // 衝突条件 2: ボードの上端を超えてしまう (上端に生成されたばかりのピースは除く)
                // nextY < 0 の場合は、落下中であれば通常衝突とは見なさない。
                if (nextY < 0) {
                    continue; // 処理をスキップ
                }
                
                // 衝突条件 3: 既に固定されたブロック (board[nextY][nextX] が 0 ではない) と重なる
                if (board[nextY] && board[nextY][nextX] !== 0) {
                    return true; // 衝突 (固定ブロック)
                }
            }
        }
    }
    return false; // 衝突なし
}

// テトリミノを1マス下に移動させる関数
function pieceDrop() {
    if (!currentPiece) return;

    // 衝突判定: 1マス下に移動したら衝突するか? (offsetX=0, offsetY=1)
    if (checkCollision(currentPiece, 0, 1)) {
        // 衝突する場合 (着地)
        
        //  ブロックをボードに固定する関数を呼び出す(次のセクションで作成)
        mergePieceToBoard(currentPiece); 
        
        checkRowClear(); 
        updateScoreDisplay()
  
        //  新しいテトリミノを生成
        currentPiece = createPiece();
  
    } else {
        // 衝突しない場合
        currentPiece.pos.y++; // 1マス下に移動
    }
}

/**
 * 現在のテトリミノをボード(2次元配列)に固定する
 * @param {object} piece - 現在のテトリミノ
 */
function mergePieceToBoard(piece) {
    piece.matrix.forEach((row, y) => {
        row.forEach((value, x) => {
            if (value !== 0) {
                // テトリミノのブロックがあれば、そのボード上の座標を value で上書き
                const boardY = piece.pos.y + y;
                const boardX = piece.pos.x + x;
                
                // 既に board[y] が存在することを確認(念のため)
                if (board[boardY]) {
                    // valueはブロックの色を表すTETROMINOS配列のインデックスを使用しても良い
                    board[boardY][boardX] = value; 
                }
            }
        });
    });
}

function handleKeyInput(event) {
    if (!currentPiece) return;

    switch (event.key) {
        case 'ArrowLeft':
            // 左に移動 (offsetX: -1, offsetY: 0)
            pieceMove(-1, 0);
            break;
        case 'ArrowRight':
            // 右に移動 (offsetX: 1, offsetY: 0)
            pieceMove(1, 0);
            break;
        case 'ArrowDown':
            // 落下を速める (ソフトドロップ)
            pieceMove(0, 1);
            // 落下を速めたら、ドロップカウンターをリセットしてすぐに次の落下を促す
            dropCounter = 0; 
            break;
        case ' ': // スペースキー (ハードドロップ)
            // ハードドロップ(一気に底まで落とす)
            hardDrop(); 
            break;
        case 'ArrowUp':
        case 'x':
            // 回転処理(次のステップで実装)
            pieceRotate(); 
            break;
    }
}

/**
 * テトリミノを指定されたオフセット分移動させる
 * @param {number} offsetX - X軸方向の移動量
 * @param {number} offsetY - Y軸方向の移動量
 */
function pieceMove(offsetX, offsetY) {
    // 衝突判定を行い、移動可能かチェック
    if (!checkCollision(currentPiece, offsetX, offsetY)) {
        // 衝突しない場合のみ、位置を更新
        currentPiece.pos.x += offsetX;
        currentPiece.pos.y += offsetY;
    } else if (offsetY === 1) {
        // 下への移動(ソフトドロップ)で衝突した場合は、着地したと見なす
        // pieceDrop()が既に衝突判定を持っているので、ここでは単に終了
    }
}

function hardDrop() {
    if (!currentPiece) return;

    // 衝突するまで繰り返し1マスずつ下に移動させる
    while (!checkCollision(currentPiece, 0, 1)) {
        currentPiece.pos.y++;
    }

    // 衝突したら、強制的に着地処理(固定と新規生成)を行う
    mergePieceToBoard(currentPiece);

   // 行を削除、得点追加
     checkRowClear(); 
     updateScoreDisplay()
    
    currentPiece = createPiece();
   
}

/**
 * 行列を90度右に回転させる
 * (y, x) の要素は (x, size - 1 - y) に移動する
 * @param {Array<Array<number>>} matrix - 回転させたい行列
 * @returns {Array<Array<number>>} 回転後の新しい行列
 */
function rotateMatrix(matrix) {
    const size = matrix.length;
    // 新しい行列を初期化(すべて0)
    const newMatrix = Array(size).fill(0).map(() => Array(size).fill(0)); 

    for (let y = 0; y < size; y++) {
        for (let x = 0; x < size; x++) {
            // 回転のロジック: [y][x] の要素を [x][size - 1 - y] に移動
            newMatrix[x][size - 1 - y] = matrix[y][x];
        }
    }
    return newMatrix;
}

// キー入力処理関数 (handleKeyInput) で 'ArrowUp' または 'x' が押された時に呼び出す
function pieceRotate() {
    if (!currentPiece) return;

    // 1. 回転後の行列を取得
    const rotatedMatrix = rotateMatrix(currentPiece.matrix);
    
    // 2. 一時的にテトリミノの行列を回転後のものに置き換え
    const originalMatrix = currentPiece.matrix;
    currentPiece.matrix = rotatedMatrix;
    
    // 3. 回転後の位置で衝突判定を行う
    if (checkCollision(currentPiece, 0, 0)) {
        // 衝突する場合: 元に戻す (Wall Kickは次のステップで考慮)
        currentPiece.matrix = originalMatrix;
    } 
    
    // 衝突しない場合: currentPiece.matrix は rotatedMatrix のまま確定
}

// キーボードイベントの監視を設定
document.addEventListener('keydown', handleKeyInput);


// スコアを保持する変数
let score = 0; 
// 1行クリアあたりの基本スコア (適当な値)
const POINTS_PER_LINE = 10; 

function checkRowClear() {
    let linesCleared = 0; // 今回クリアした行数

    // ボードの最も下 (BOARD_HEIGHT - 1) から上 (0) に向かってチェックする
    outer: for (let y = BOARD_HEIGHT - 1; y >= 0; y--) {
        // 現在の行 (board[y]) の各マスをチェック
        for (let x = 0; x < BOARD_WIDTH; x++) {
            // 1つでも空白マス (0) があれば、この行はクリアされていない
            if (board[y][x] === 0) {
                continue outer; // 次の行のチェックへ移動
            }
        }

        // ここに到達した場合、その行 (y) は完全にブロックで埋まっている (クリア対象)

        // 1. 行を削除する
        // board.splice(y, 1) で y行目から1行分を削除
        board.splice(y, 1);
        
        // 2. 削除した分の新しい空白行をボードの最上部に追加する
        // Array(BOARD_WIDTH).fill(0) で、幅分の空白行を作成
        board.unshift(new Array(BOARD_WIDTH).fill(0)); 
        
        // 3. 削除処理をしたので、yのループを再度実行するため y をインクリメント (重要!)
        // spliceで配列の要素数が減り、yの位置が変わったため
        y++; 
        linesCleared++;
    }

    // スコア計算
    if (linesCleared > 0) {
        // 例: 複数行同時クリアはボーナス
        score += POINTS_PER_LINE * linesCleared * linesCleared; 
        console.log(`クリア: ${linesCleared}行! スコア: ${score}`);
        // 画面のスコア表示を更新する関数を後で追加
    }
}

function updateScoreDisplay() {
    document.getElementById('score-value').innerText = score;
}


// --- ゲーム開始 ---
setInterval(gameLoop,1000)
currentPiece = createPiece(); // 最初のテトリミノを生成
       
     
  </script> 
</body>
</html>

いいなと思ったら応援しよう!