見出し画像

テトリスを作る - 4 キー入力

着地して固定されるようになったので、次はテトリスをゲームとして成立させるための重要な要素、**プレイヤーによる操作(キー入力)**を実装します。


ステップ 7: キー入力による操作の実装

キーボードの矢印キーを使って、テトリミノを左右に移動させ、また下方向へ瞬時に落とす処理を実装します。

キーイベントリスナーの追加

まず、コードの末尾に、キーボードの入力を監視するイベントリスナーを追加します。

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

キー入力処理関数 handleKeyInput の作成

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;
    }
}

この関数内で、押されたキーに応じてテトリミノを動かします。テトリミノがない条件では動きません。

if (!currentPiece) return;

動かしたい方向のキーボードを押すと"pieceMove(-1, 0);"関数を使って座標の移動をします。

case 'ArrowLeft':
   pieceMove(-1, 0);
   break;

テトリミノを移動させる関数 pieceMove の作成

左右の移動とソフトドロップ(速い落下)は、この汎用的な関数で行います。

/**
 * テトリミノを指定されたオフセット分移動させる
 * @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()が既に衝突判定を持っているので、ここでは単に終了
    }
}

衝突判定して衝突していない条件で座標を変更します。

if (!checkCollision(currentPiece, offsetX, offsetY))

キーボードを押した分の"offsetX"を受けてその分追加します。

currentPiece.pos.x += offsetX;


ハードドロップ(一瞬で着地)のロジック

スペースキーを押されたとき、ブロックが着地するまで繰り返し下に移動させます。

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

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

    // 衝突したら、強制的に着地処理(固定と新規生成)を行う
    mergePieceToBoard(currentPiece);
    // checkRowClear(); // 次のステップで実装
    
    currentPiece = createPiece();
    
    // ゲームオーバー判定
    if (checkCollision(currentPiece, 0, 0)) {
        console.log("ゲームオーバー!");
        // ゲームループを停止する処理などを追加
    }
}

これで、キーボードを使ってブロックを左右に動かし、ソフトドロップやハードドロップができるようになりました。

テトリミノの回転の実装

行列を回転させる関数 ・・・ rotateMatrix(matrix)

テトリミノの形状を表す行列(配列の配列)を受け取り、それを90度右に回転させた新しい行列を返す関数

回転のロジック: [y][x] の要素を [x][size - 1 - y]

/**
 * 行列を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;
}

実際に回転操作する関数 ・・・  pieceRotate

キー入力で呼び出される関数です。回転後の形状で衝突判定を行い、問題なければテトリミノを回転させます。

// キー入力処理関数 (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 のまま確定
}


これまでの全コード

<!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; /* 枠線をつけてゲームボードを分かりやすくします */
        }
    </style>

</head>
<body>
    <canvas id="tetris-canvas"></canvas>
    
    <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); 
   
    // if (currentPiece) {
    //     currentPiece.pos.y++;
    // }

    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); 
        
  
        //  新しいテトリミノを生成
        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(); // 次のステップで実装
    
    currentPiece = createPiece();
    
    // ゲームオーバー判定
    if (checkCollision(currentPiece, 0, 0)) {
        console.log("ゲームオーバー!");
        // ゲームループを停止する処理などを追加
    }
}

/**
 * 行列を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);


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

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