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

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

第 6 回です。今回は仕様 ⑥ テトリミノは画面最下部または他ブロックの上部に面したとき動かせなくなる と ⑦ テトリミノは画面上部に生成される さらに ⑧ テトリミノは一定間隔で下方向に移動する を実装していきます。

① 画面サイズは縦 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)  
 ⑥ テトリミノは画面最下部または他ブロックの上部に面したとき動かせなくなる  
 ⑦ テトリミノは画面上部に生成される  
 ⑧ テトリミノは一定間隔で下方向に移動する  
 ⑨ テトリミノはランダムに生成される →[その 3 参照](https://qiita.com/namakoRice/items/fd4489b04b511021bd7e)  
 ⑩ 横 10 ブロック分がそろった場合、そろった列は消えて上の列が下りてくる  
 ⑪ テトリミノ生成場所にブロックがある場合、ゲームが終了する

## テトリスを画面上部で生成させる

テトリスを生成する関数を作成していきます。まずは、テスト用の固定ブロックが邪魔なので消してしまいましょう。その上で次のようなコードを記述します。

### コード

```javascript
・省略・
// 画面を真ん中にする
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();
  drawPlayScreen();
};

・省略・
```

### 解説

createTetPosition によってテトリスの生成位置を決めています。生成時は Y 軸方向に対しては移動量が 0 X 軸方向に対しては画面の真ん中に来てほしいので、移動量はスクリーンの半分の位置にしています。ただし。テトリミノ＝ 4×4 の四角の始点は左端となるため、このままでは若干右にずれてしまいます。そこで、四角の真ん中分左にずらしてあげるように TET_SIZE/2 で調整してあげています。生成のタイミングは初期処理時とブロックが動かなくなったあとの 2 回あります。なので、まずは初期処理 init の中で関数を動かしています。

## 自動で落下させる

続いて自動で落下させていきます。

### コード

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

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

// フィールドのサイズ
const PLAY_SCREEN_WIDTH = 10;
const PLAY_SCREEN_HEIGHT = 20;
・省略・
// 落下処理
const dropTet = () => {
  if (canMove(0, 1)) {
    tetroMinoDistanceY++;
  }
  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();
};

・省略・
```

### 解説

まずは落下処理 dropTet を作っていきます。下に落ちていくわけなので tetroMinoDistanceY の値を増やせばいいだけです。ただし、それ以上下に行けるか確認する必要はあるので CanMove で下方向に対する確認を行っています。  
移動後のテトリミノの情報を画面側に反映させる必要があるため、処理の最後に drawPlayScreen を使って反映させます。  
実際の処理実行についてはまずは初期化処理 init 内で実施します。ここで setInterbal を使用する。これは第一引数に実行させたい関数名を、第二引数に実行間隔（ミリ秒）を入れることで、ミリ秒単位で繰り返し関数を実行してくれるというモノです。今回は 300 ミリ秒ごとにテトリミノを 1 ブロック下げるという形になっています。
実際に自動で下に移動されるようになったら成功です。

## 下に移動できなくなったテトリミノを固定する

最後にこれ以上下に移動できなくなった際にテトリミノを固定させる処理を書いていきます。画面上に動かなくなったブロックを描くロジックはすでに実装してあります。ただし、描画処理であって実際にどの位置に固定させるか という情報を別途持たせる必要があります。 ・・・課題その 6
つまりこれ以上下に移動出来なくなった＝スクリーン上（20×10 の四角の中）のどこかのマスの値が 1 になっているという情報を持たせる必要がある ということです。

### コード

```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 dropTet = () => {
  if (canMove(0, 1)) {
    tetroMinoDistanceY++;
  } else {
    fixTet();
    createTetPosition();
  }
  drawPlayScreen();
};
・省略・
```

### 解説

処理自体は単純です。現在のテトリミノの情報を確認し、移動量分（tetroMinoDistanceY/X）だけ加えたスクリーン上のブロックの位置の値を 1 に変換しているだけです。呼び出すのは、dropTet でこれ以上下に移動出来ないと判断されたときです。固定後は createStartPosition によってテトリミノの生成位置を初期位置に戻しています。再生成されたテトリミノや固定されたテトリミノの実際の描画は drawPlayScreen によっておこなわれます。  
さて、このとき再生成されるテトリミノがずっと同じになってしまいます。テトリミノの固定後に再びテトリミノをランダムに選ぶ処理を加えましょう。

### コード

```javascript
・省略・
// 落下処理
const dropTet = () => {
  if (canMove(0, 1)) {
    tetroMinoDistanceY++;
  } else {
    fixTet();
    tetroTypesIndex = Math.floor(Math.random() * 7);
    tetroMino = TETRO_TYPES[tetroTypesIndex];
    createTetPosition();
  }
  drawPlayScreen();
};

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

### 解説

かなり単純です。fixTet の後に再びランダム関数と配列の選択を実施しているだけになります。

### まとめ

今回は、上部での生成と自動落下、そしてミノの固定処理を行いました。ポイントは下記のとおりです。

1. setInterbal を使うことで一定間隔で処理を実行させることができる。  
   かなりテトリスっぽくなってきました、次回はいよいよ（一旦）最終回、そろった列を消す処理とゲームオーバー処理を実装していきます。

```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],
  ],
];

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

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

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

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

// テトリスプレイ画面描画処理
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);
      }
    }
  }
};

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

  // 塗りに赤を設定
  CANVAS_2D.fillStyle = '#E33';
  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) => {
  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] = 1;
      }
    }
  }
};

// 落下処理
const dropTet = () => {
  if (canMove(0, 1)) {
    tetroMinoDistanceY++;
  } else {
    fixTet();
    tetroTypesIndex = Math.floor(Math.random() * 7);
    tetroMino = TETRO_TYPES[tetroTypesIndex];
    createTetPosition();
  }
  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();
};
```