JavaScript でテトリスを開発する その 7

# JavaScript でテトリスを開発する その 7

第 7 回です。今回は仕様 ⑩ 横 10 ブロック分がそろった場合、そろった列は消えて上の列が下りてくる と ⑪ テトリミノ生成場所にブロックがある場合、ゲームが終了する を実装していきます。

① 画面サイズは縦 20 ブロック分、横 10 ブロック分である →[その 1 参照](https://qiita.com/namakoRice/items/4933e77a41b2ac359229)
② 4 つの正方形ブロックで構成されるテトリミノがある →[その 2 参照](https://qiita.com/namakoRice/items/4fefc6557af480d8e9a5)
③ テトリミノは 7 種類ある →[その 3 参照](https://qiita.com/namakoRice/items/fd4489b04b511021bd7e)  
④ テトリミノは画面内でかつ他ブロックに干渉しなければ自由に動かせる →[その 4 参照](https://qiita.com/namakoRice/items/ae2d83b962ba7fb49939)  
 ⑤ テトリミノは画面内でかつ他ブロックに干渉しなければ自由に回転できる →[その 5 参照](https://qiita.com/namakoRice/items/2a488b4554372277c153)  
 ⑥ テトリミノは画面最下部または他ブロックの上部に面したとき動かせなくなる →[その 6 参照](https://qiita.com/namakoRice/items/caf4469e968b084e2fc1)
⑦ テトリミノは画面上部に生成される →[その 6 参照](https://qiita.com/namakoRice/items/caf4469e968b084e2fc1)
⑧ テトリミノは一定間隔で下方向に移動する →[その 6 参照](https://qiita.com/namakoRice/items/caf4469e968b084e2fc1)
⑨ テトリミノはランダムに生成される →[その 3 参照](https://qiita.com/namakoRice/items/fd4489b04b511021bd7e)  
 ⑩ 横 10 ブロック分がそろった場合、そろった列は消えて上の列が下りてくる  
 ⑪ テトリミノ生成場所にブロックがある場合、ゲームが終了する

## そろった行を消す

そろった行を消していく処理をつくります。まずはそろったかどうかを確認します。確認方法はこれまで難度も出てきた for ループを使って画面全体を確認する方法になります。「消す」か「消さないか」の判断なので boolean でフラグを立てて if 分岐してあげれば良さそうです。

### コード

```javascript
・省略・
const fixTet = () => {
  for (let y = 0; y < TET_SIZE; y++) {
    for (let x = 0; x < TET_SIZE; x++) {
      if (tetroMino[y][x]) {
        SCREEN[tetroMinoDistanceY + y][tetroMinoDistanceX + x] = 1;
      }
    }
  }
};
const clearLine = () => {
  // 一列になっている場所をスクリーン上から調べていく
  for (let y = 0; y < PLAY_SCREEN_HEIGHT; y++) {
    // 行を消すフラグを立てる
    let isClearLine = true;
    // 行に0が入っている（＝そろっていない）かを調べていく
    for (let x = 0; x < PLAY_SCREEN_WIDTH; x++) {
      if (SCREEN[y][x] === 0) {
        isClearLine = false;
        break;
      }
    }
    if(isClearLine){
        // 削除処理
    }
  }
};
・省略・
```

### 解説

というわけでいつも通り for ループで画面全体を 1 ブロックずつチェックしていきます。1 行がすべてそろっている＝その行には 1 つも値が 0 のものがない ということが成り立ちます。よってつまり 1 つでも 0 の値が入っている行があれば消されないようにしなければなりません。そこで、1 行単位（y の値が増える度）で削除フラグを立てます。そして 1 ブロック単位（x の値が増える度）に値のチェックを行います。もし、特定のブロックに 0 が含まれていた場合、フラグを false にし、break によって内側の for ループから抜け出します。ここで混同しやすいのですが、break はあくまで最も内側のループ処理を抜ける宣言なので、if 分岐だけ抜けるわけではありません。要注意です。  
もし、行がすべて 1 である場合は if に引っかからず、フラグが true のまま次の処理に進むという形になります。  
皆さんならどのように削除処理を記載するでしょうか・・・課題その 7  
さて、次に削除処理です。ここで、消すのだから、該当する列の値をすべて 0 にしたらいいのでは？と考えた方もいるかもしれません。しかし、削除した後、上の行が下に下りてくる必要があります。それなら、わざわざ消さなくとも上の行の情報を下の行へコピペすればよいと分かります。そこで次のような処理を記述します。

### コード

```javascript
・省略・
const clearLine = () => {
  // 一列になっている場所をスクリーン上から調べていく
  for (let y = 0; y < PLAY_SCREEN_HEIGHT; y++) {
    // 行を消すフラグを立てる
    let isClearLine = true;
    // 行に0が入っている（＝そろっていない）かを調べていく
    for (let x = 0; x < PLAY_SCREEN_WIDTH; x++) {
      if (SCREEN[y][x] === 0) {
        isClearLine = false;
        break;
      }
    }
    if (isClearLine) {
      // そろった行から上へ向かってforループしていく
      for (let newY = y; newY > 0; newY--) {
        for (let newX = 0; newX < PLAY_SCREEN_WIDTH; newX++) {
          // 一列上の情報をコピーする
          SCREEN[newY][newX] = SCREEN[newY - 1][newX];
        }
      }
    }
  }
};
・省略・
// 落下処理
const dropTet = () => {
  if (canMove(0, 1)) {
    tetroMinoDistanceY++;
  } else {
    fixTet();
    clearLine();
    tetroTypesIndex = Math.floor(Math.random() * 7);
    tetroMino = TETRO_TYPES[tetroTypesIndex];
    createTetPosition();
  }
  drawPlayScreen();
};
・省略・
```

### 解説

これまで通り、for ループによってスクリーン全体を確認していきます。ただし、今回はスクリーン全体を見る必要はありません。消される行を起点にそれより上だけを調査すればよいのです。なぜなら、消された後に落ちてくる（処理が必要な）のはそれより上の行だけだからです。  
そこで newY には、消された時点の行 y を設定します。上に上がっていきたいので、1 つずつ値を引いていきます。x 方向は同じ見方をするので変化はありません。  
最後に、スクリーンに今の行から 1 つ上の行の情報をコピーしていきます。関数の呼び出しは落下処理側で実施します。  
実際に一列消すことができたら成功です。

## GameOver を作る

### コード

```javascript
・省略・
// テトリミノの移動距離
let tetroMinoDistanceX = 0;
let tetroMinoDistanceY = 0;

// 画面本体
const SCREEN = [];

// タイマーID
let timerId = NaN;

// ゲームオーバーフラグ
let isGameOver = false;
・省略・
document.onkeydown = (e) => {
  if (isGameOver) return;
  switch (e.code) {
    case 'ArrowLeft':
      if (canMove(-1, 0)) tetroMinoDistanceX--;
      break;
    case 'ArrowUp':
      if (canMove(0, -1)) tetroMinoDistanceY--;
      break;
    case 'ArrowRight':
      if (canMove(1, 0)) tetroMinoDistanceX++;
      break;
    case 'ArrowDown':
      if (canMove(0, 1)) tetroMinoDistanceY++;
      break;
    case 'KeyR':
      let newRTet = createRightRotateTet();
      if (canMove(0, 0, newRTet)) {
        tetroMino = newRTet;
      }
      break;
    case 'KeyL':
      let newLTet = createLeftRotateTet();
      if (canMove(0, 0, newLTet)) {
        tetroMino = newLTet;
      }
      break;
  }
  drawPlayScreen();
};

・省略・

// 落下処理
const dropTet = () => {
  if (isGameOver) return;
  if (canMove(0, 1)) {
    tetroMinoDistanceY++;
  } else {
    fixTet();
    clearLine();
    tetroTypesIndex = Math.floor(Math.random() * 7);
    tetroMino = TETRO_TYPES[tetroTypesIndex];
    createTetPosition();
    // 次のテトリミノを出せなくなったらゲームオーバー
    if (!canMove(0, 0)) {
      isGameOver = true;
      clearInterval(timerId);
    }
  }
  drawPlayScreen();
};
・省略・
```

### 解説

まずは、ゲームオーバーフラグを作ります。フラグを立てる場所は落下処理後のテトリミノ再生成時です。生成した箇所にブロックがあった場合、フラグを true にして、cleaInterval を発動します。これは引数に NaN を入れることで、現在実行されている setInterval を停止させることができます。さらに、フラグが true としたことで操作キーの受付や落下処理の受付も拒否しています。  
これでテトリスのゲーム自体は終了させられますが、少し寂しいので文字をいれていきます。

### コード

```javascript
・省略・
// テトリスプレイ画面描画処理
const drawPlayScreen = () => {
  // 背景色を黒に指定
  CANVAS_2D.fillStyle = '#000';

  // キャンバスを塗りつぶす
  CANVAS_2D.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);

  // x,y =100, 100の場所に30×30のブロックを描画
  //CANVAS_2D.fillRect(100, 100, BLOCK_SIZE, BLOCK_SIZE);

  // 画面本体で動かせなくなったテトリミノを描画する
  for (let y = 0; y < PLAY_SCREEN_HEIGHT; y++) {
    for (let x = 0; x < PLAY_SCREEN_WIDTH; x++) {
      if (SCREEN[y][x]) {
        drawBlock(x, y);
      }
    }
  }

  // テトリミノを描画する
  for (let y = 0; y < TET_SIZE; y++) {
    for (let x = 0; x < TET_SIZE; x++) {
      if (tetroMino[y][x]) {
        drawBlock(tetroMinoDistanceX + x, tetroMinoDistanceY + y);
      }
    }
  }

  if (isGameOver) {
    const GAME_OVER_MESSAGE = 'GAME OVER';
    CANVAS_2D.font = "40px 'Meiryo UI'";
    const width = CANVAS_2D.measureText(GAME_OVER_MESSAGE).width;
    const x = CANVAS_WIDTH / 2 - width / 2;
    const y = CANVAS_HEIGHT / 2 - 20;
    CANVAS_2D.fillStyle = 'white';
    CANVAS_2D.fillText(GAME_OVER_MESSAGE, x, y);
  }
  ・省略・
};
```

### 解説

画面の描画処理内に、ゲームオーバーフラグがたった場合のみ、文字列を表示させるようにしています。  
font では、フォントサイズとタイプを指定できます。measureText.width では文字列の幅を取得することができます。x/y で文字を表示させる位置を指定しています。あとは、これまでと同じような処理となります。

さて、これで全ての機能が実装できました。最後に各テトリミノに違う色を付けて完成としましょう。

### コード

```javascript
・省略・
const tetColors = ['#6CF', '#F92', '#66F', '#C5C', '#FD2', '#F44', '#5B5'];

// TETRO_TYPESのインデックス番号をランダム取得
let tetroTypesIndex = Math.floor(Math.random() * 7);

// テトロミノを取得する
let tetroMino = TETRO_TYPES[tetroTypesIndex];
・省略・
// テトリスプレイ画面描画処理
const drawPlayScreen = () => {
  // 背景色を黒に指定
  CANVAS_2D.fillStyle = '#000';

  // キャンバスを塗りつぶす
  CANVAS_2D.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);

  // 画面本体で動かせなくなったテトリミノを描画する
  for (let y = 0; y < PLAY_SCREEN_HEIGHT; y++) {
    for (let x = 0; x < PLAY_SCREEN_WIDTH; x++) {
      if (SCREEN[y][x]) {
        drawBlock(x, y, SCREEN[y][x]);
      }
    }
  }

  // テトリミノを描画する
  for (let y = 0; y < TET_SIZE; y++) {
    for (let x = 0; x < TET_SIZE; x++) {
      if (tetroMino[y][x]) {
        drawBlock(
          tetroMinoDistanceX + x,
          tetroMinoDistanceY + y,
          tetroTypesIndex
        );
      }
    }
  }

  if (isGameOver) {
    const GAME_OVER_MESSAGE = 'GAME OVER';
    CANVAS_2D.font = "40px 'Meiryo UI'";
    const width = CANVAS_2D.measureText(GAME_OVER_MESSAGE).width;
    const x = CANVAS_WIDTH / 2 - width / 2;
    const y = CANVAS_HEIGHT / 2 - 20;
    CANVAS_2D.fillStyle = 'white';
    CANVAS_2D.fillText(GAME_OVER_MESSAGE, x, y);
  }
};

const drawBlock = (x, y, tetroTypesIndex) => {
  let drawX = x * BLOCK_SIZE;
  let drawY = y * BLOCK_SIZE;

  // 塗りに赤を設定
  CANVAS_2D.fillStyle = tetColors[tetroTypesIndex];
  CANVAS_2D.fillRect(drawX, drawY, BLOCK_SIZE, BLOCK_SIZE);
  // 線の色を黒に設定
  CANVAS_2D.strokeStyle = 'black';
  CANVAS_2D.strokeRect(drawX, drawY, BLOCK_SIZE, BLOCK_SIZE);
};
・省略・
const fixTet = () => {
  for (let y = 0; y < TET_SIZE; y++) {
    for (let x = 0; x < TET_SIZE; x++) {
      if (tetroMino[y][x]) {
        SCREEN[tetroMinoDistanceY + y][tetroMinoDistanceX + x] =
          tetroTypesIndex;
      }
    }
  }
};
・省略・
```

### 解説

まず、色用の配列を用意します。実際の色付けはこれまで通り、描写処理 drawBlock で行います。ブロックごとに色を固定したいため、ランダムで取得したインデックス番号をそのままカラー配列の値指定に使用します。そのために引数には tetroTypesIndex を追加しています。引数が追加されたので、処理の呼び元にも引数を追記していきます。  
まず一つ目の呼び元 動かなくなったテトリミノの描画です。ここでポイントは動かなくなったミノの色を保持したままにしたい。ということです。そこで fixTet の最後に SCREEN 配列に入れていた値 1 を tetroTypesIndex にします。そうすることで、SCREEN 配列にはテトリミノが固定された瞬間にその固定されたミノのインデックス番号を持つことができます。SCREEN 配列にインデックス番号情報があるので、そいつを引数として渡してあげれば、固定後も色が変化させないでおくことができます。ここの処理の流れが分かりにくいと感じたかたは、fixTet の SCREEN 配列に設定する値を 0~6 のどれかに固定してみましょう。  
二つ目の呼び元はインデックス番号を入れてあげます。ここの描画処理はテトリミノ生成時の描画処理なので現在のインデックス番号が渡されていれば問題ありません。  
ここで、テトリスをプレイすると、Z 型のミノが消滅するバグが発生します。これはなぜでしょうか。 ・・・課題その 8
答えは簡単です。SCREEN 配列の値が 0 の時、描画処理上での if 判定が false になる（＝描画されない）からです。そこで、テトリミノと色の配列の 0 番目はダミー、つまり何も生成しないし、色も付かない値を入れ込みます。

### コード

```javascript
・省略・
let TETRO_TYPES = [
  [],
  [
    // Z
    [0, 0, 0, 0],
    [1, 1, 0, 0],
    [0, 1, 1, 0],
    [0, 0, 0, 0],
  ],
  [
    // S
    [0, 0, 0, 0],
    [0, 0, 1, 1],
    [0, 1, 1, 0],
    [0, 0, 0, 0],
  ],
  [
    // I
    [0, 0, 0, 0],
    [1, 1, 1, 1],
    [0, 0, 0, 0],
    [0, 0, 0, 0],
  ],
  [
    // J
    [0, 1, 0, 0],
    [0, 1, 0, 0],
    [0, 1, 1, 0],
    [0, 0, 0, 0],
  ],
  [
    // L
    [0, 0, 1, 0],
    [0, 0, 1, 0],
    [0, 1, 1, 0],
    [0, 0, 0, 0],
  ],
  [
    // T
    [0, 0, 0, 0],
    [1, 1, 1, 0],
    [0, 1, 0, 0],
    [0, 0, 0, 0],
  ],
  [
    // O
    [0, 0, 0, 0],
    [0, 1, 1, 0],
    [0, 1, 1, 0],
    [0, 0, 0, 0],
  ],
];

const tetColors = ['', '#6CF', '#F92', '#66F', '#C5C', '#FD2', '#F44', '#5B5'];

// TETRO_TYPESのインデックス番号をランダム取得
let tetroTypesIndex = Math.floor(Math.random() * (TETRO_TYPES.length - 1)) + 1;
・省略・
// 落下処理
const dropTet = () => {
  if (isGameOver) return;
  if (canMove(0, 1)) {
    tetroMinoDistanceY++;
  } else {
    fixTet();
    clearLine();
    tetroTypesIndex = Math.floor(Math.random() * (TETRO_TYPES.length - 1)) + 1;
    tetroMino = TETRO_TYPES[tetroTypesIndex];
    createTetPosition();
    // 次のテトリミノを出せなくなったらゲームオーバー
    if (!canMove(0, 0)) {
      isGameOver = true;
      clearInterval(timerId);
    }
  }
  drawPlayScreen();
};
・省略・
```

### 解説

それぞれの配列の先頭に空の値をいれました。インデックス番号をランダムで取得する際に、この空の値が指定されては困るので、ランダムに取得する値を 1~7 に変更させています。  
考え方は配列の長さ（8 個）なので 0 ～ 8 がまずとれます。0 を除外したいので、最後に+1 をします。しかし、最大値が 8 となりエラーが発生してしまうため、事前に-1 をしておきます。こうすることで 1~7 の値をランダムで取得できるようになります。ランダム関数の扱いが分からなくなった方は[コチラ](https://qiita.com/namakoRice/items/fd4489b04b511021bd7e)

## 完成！！

これでテトリスが完成しました！おめでとうございます！とはいえ基本的な機能しか入れてないのでまだまだ改良の余地はあります。改良する様子も引き続き解説出来ればとは思いますが、一旦はこれで JavaScript でテトリスを開発する は終了とさせていただきます。  
やはり一番の壁は回転動作の所だと思います。詰まったところ、理解が出来なかったところはいきなり関数化するのではなく、手続き型で適当な値を当てはめながらやってみたり、図に起こしてみたりすると理解しやすくなるとおもいます！最後に完成したコードを記載します。 ここもっとわかりやすく解説してほしい！みたいな要望があればぜひコメントください！ 
よき開発ライフを

```javascript
// 落下スピード
const DROP_SPEED = 300;

// 1ブロックの大きさ
const BLOCK_SIZE = 30;

// フィールドのサイズ
const PLAY_SCREEN_WIDTH = 10;
const PLAY_SCREEN_HEIGHT = 20;

// キャンバスIDの取得
const CANVAS = document.getElementById('canvas');

// 2dコンテキストの取得
const CANVAS_2D = CANVAS.getContext('2d');

// キャンバスサイズ（＝プレイ画面のサイズ）
const CANVAS_WIDTH = BLOCK_SIZE * PLAY_SCREEN_WIDTH;
const CANVAS_HEIGHT = BLOCK_SIZE * PLAY_SCREEN_HEIGHT;
CANVAS.width = CANVAS_WIDTH;
CANVAS.height = CANVAS_HEIGHT;

// テトリミノの1辺の最長
const TET_SIZE = 4;

// 7種類のテトリミノ達
let TETRO_TYPES = [
  [],
  [
    // Z
    [0, 0, 0, 0],
    [1, 1, 0, 0],
    [0, 1, 1, 0],
    [0, 0, 0, 0],
  ],
  [
    // S
    [0, 0, 0, 0],
    [0, 0, 1, 1],
    [0, 1, 1, 0],
    [0, 0, 0, 0],
  ],
  [
    // I
    [0, 0, 0, 0],
    [1, 1, 1, 1],
    [0, 0, 0, 0],
    [0, 0, 0, 0],
  ],
  [
    // J
    [0, 1, 0, 0],
    [0, 1, 0, 0],
    [0, 1, 1, 0],
    [0, 0, 0, 0],
  ],
  [
    // L
    [0, 0, 1, 0],
    [0, 0, 1, 0],
    [0, 1, 1, 0],
    [0, 0, 0, 0],
  ],
  [
    // T
    [0, 0, 0, 0],
    [1, 1, 1, 0],
    [0, 1, 0, 0],
    [0, 0, 0, 0],
  ],
  [
    // O
    [0, 0, 0, 0],
    [0, 1, 1, 0],
    [0, 1, 1, 0],
    [0, 0, 0, 0],
  ],
];

const tetColors = ['', '#6CF', '#F92', '#66F', '#C5C', '#FD2', '#F44', '#5B5'];

// TETRO_TYPESのインデックス番号をランダム取得
let tetroTypesIndex = Math.floor(Math.random() * (TETRO_TYPES.length - 1)) + 1;

// テトロミノを取得する
let tetroMino = TETRO_TYPES[tetroTypesIndex];

// テトリミノの移動距離
let tetroMinoDistanceX = 0;
let tetroMinoDistanceY = 0;

// 画面本体
const SCREEN = [];

// タイマーID
let timerId = NaN;

// ゲームオーバーフラグ
let isGameOver = false;

// テトリスプレイ画面描画処理
const drawPlayScreen = () => {
  // 背景色を黒に指定
  CANVAS_2D.fillStyle = '#000';

  // キャンバスを塗りつぶす
  CANVAS_2D.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);

  // 画面本体で動かせなくなったテトリミノを描画する
  for (let y = 0; y < PLAY_SCREEN_HEIGHT; y++) {
    for (let x = 0; x < PLAY_SCREEN_WIDTH; x++) {
      if (SCREEN[y][x]) {
        drawBlock(x, y, SCREEN[y][x]);
      }
    }
  }

  // テトリミノを描画する
  for (let y = 0; y < TET_SIZE; y++) {
    for (let x = 0; x < TET_SIZE; x++) {
      if (tetroMino[y][x]) {
        drawBlock(
          tetroMinoDistanceX + x,
          tetroMinoDistanceY + y,
          tetroTypesIndex
        );
      }
    }
  }

  if (isGameOver) {
    const GAME_OVER_MESSAGE = 'GAME OVER';
    CANVAS_2D.font = "40px 'Meiryo UI'";
    const width = CANVAS_2D.measureText(GAME_OVER_MESSAGE).width;
    const x = CANVAS_WIDTH / 2 - width / 2;
    const y = CANVAS_HEIGHT / 2 - 20;
    CANVAS_2D.fillStyle = 'white';
    CANVAS_2D.fillText(GAME_OVER_MESSAGE, x, y);
  }
};

const drawBlock = (x, y, tetroTypesIndex) => {
  let drawX = x * BLOCK_SIZE;
  let drawY = y * BLOCK_SIZE;

  // 塗りに赤を設定
  CANVAS_2D.fillStyle = tetColors[tetroTypesIndex];
  CANVAS_2D.fillRect(drawX, drawY, BLOCK_SIZE, BLOCK_SIZE);
  // 線の色を黒に設定
  CANVAS_2D.strokeStyle = 'black';
  CANVAS_2D.strokeRect(drawX, drawY, BLOCK_SIZE, BLOCK_SIZE);
};

const canMove = (moveX, moveY, newTet = tetroMino) => {
  for (let y = 0; y < TET_SIZE; y++) {
    for (let x = 0; x < TET_SIZE; x++) {
      if (newTet[y][x]) {
        // 現在のテトリミノの位置（tetroMinoDistanceX + x）に移動分を加える（＝移動後の座標）
        let nextX = tetroMinoDistanceX + x + moveX;
        let nextY = tetroMinoDistanceY + y + moveY;

        // 移動先にブロックがあるか判定
        if (
          nextY < 0 ||
          nextX < 0 ||
          nextY >= PLAY_SCREEN_HEIGHT ||
          nextX >= PLAY_SCREEN_WIDTH ||
          SCREEN[nextY][nextX]
        ) {
          return false;
        }
      }
    }
  }
  return true;
};

// 右回転
const createRightRotateTet = () => {
  //回転後の新しいテトリミノ用配列
  let newTet = [];
  for (let y = 0; y < TET_SIZE; y++) {
    newTet[y] = [];
    for (let x = 0; x < TET_SIZE; x++) {
      newTet[y][x] = tetroMino[TET_SIZE - 1 - x][y];
    }
  }
  return newTet;
};

// 左回転
const createLeftRotateTet = () => {
  //回転後の新しいテトリミノ用配列
  let newTet = [];
  for (let y = 0; y < TET_SIZE; y++) {
    newTet[y] = [];
    for (let x = 0; x < TET_SIZE; x++) {
      newTet[y][x] = tetroMino[x][TET_SIZE - 1 - y];
    }
  }
  return newTet;
};

document.onkeydown = (e) => {
  if (isGameOver) return;
  switch (e.code) {
    case 'ArrowLeft':
      if (canMove(-1, 0)) tetroMinoDistanceX--;
      break;
    case 'ArrowUp':
      if (canMove(0, -1)) tetroMinoDistanceY--;
      break;
    case 'ArrowRight':
      if (canMove(1, 0)) tetroMinoDistanceX++;
      break;
    case 'ArrowDown':
      if (canMove(0, 1)) tetroMinoDistanceY++;
      break;
    case 'KeyR':
      let newRTet = createRightRotateTet();
      if (canMove(0, 0, newRTet)) {
        tetroMino = newRTet;
      }
      break;
    case 'KeyL':
      let newLTet = createLeftRotateTet();
      if (canMove(0, 0, newLTet)) {
        tetroMino = newLTet;
      }
      break;
  }
  drawPlayScreen();
};

const fixTet = () => {
  for (let y = 0; y < TET_SIZE; y++) {
    for (let x = 0; x < TET_SIZE; x++) {
      if (tetroMino[y][x]) {
        SCREEN[tetroMinoDistanceY + y][tetroMinoDistanceX + x] =
          tetroTypesIndex;
      }
    }
  }
};

const clearLine = () => {
  // 一列になっている場所をスクリーン上から調べていく
  for (let y = 0; y < PLAY_SCREEN_HEIGHT; y++) {
    // 行を消すフラグを立てる
    let isClearLine = true;
    // 行に0が入っている（＝そろっていない）かを調べていく
    for (let x = 0; x < PLAY_SCREEN_WIDTH; x++) {
      if (SCREEN[y][x] === 0) {
        isClearLine = false;
        break;
      }
    }
    if (isClearLine) {
      // そろった行から上へ向かってforループしていく
      for (let newY = y; newY > 0; newY--) {
        for (let newX = 0; newX < PLAY_SCREEN_WIDTH; newX++) {
          // 一列上の情報をコピーする
          SCREEN[newY][newX] = SCREEN[newY - 1][newX];
        }
      }
    }
  }
};

// 落下処理
const dropTet = () => {
  if (isGameOver) return;
  if (canMove(0, 1)) {
    tetroMinoDistanceY++;
  } else {
    fixTet();
    clearLine();
    tetroTypesIndex = Math.floor(Math.random() * (TETRO_TYPES.length - 1)) + 1;
    tetroMino = TETRO_TYPES[tetroTypesIndex];
    createTetPosition();
    // 次のテトリミノを出せなくなったらゲームオーバー
    if (!canMove(0, 0)) {
      isGameOver = true;
      clearInterval(timerId);
    }
  }
  drawPlayScreen();
};

// 画面を真ん中にする
const CONTAINER = document.getElementById('container');
CONTAINER.style.width = CANVAS_WIDTH + 'px';

const createTetPosition = () => {
  tetroMinoDistanceX = PLAY_SCREEN_WIDTH / 2 - TET_SIZE / 2;
  tetroMinoDistanceY = 0;
};

// 初期化処理
const init = () => {
  for (let y = 0; y < PLAY_SCREEN_HEIGHT; y++) {
    SCREEN[y] = [];
    for (let x = 0; x < PLAY_SCREEN_WIDTH; x++) {
      SCREEN[y][x] = 0;
    }
  }

  // テスト用
  //SCREEN[4][6] = 1;
  createTetPosition();
  // 落下処理実行
  setInterval(dropTet, DROP_SPEED);
  drawPlayScreen();
};
```