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

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

第 3 回です。今回は仕様 ④ テトリミノは画面内でかつ他ブロックに干渉しなければ自由に動かせる を実装していきます。

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

## テトリミノを画面内で自由に動かす

キーボードの矢印ボタンを使ってテトリミノを動かせるようにしていきます。

### コード

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

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

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

  // 塗りに赤を設定
  CANVAS_2D.fillStyle = '#E33';
  // x,y =100, 100の場所に30×30のブロックを描画
  CANVAS_2D.fillRect(100, 100, BLOCK_SIZE, BLOCK_SIZE);

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

document.onkeydown = (e) => {
  switch (e.code) {
    case 'ArrowLeft':
      tetroMinoDistanceX--;
      break;
    case 'ArrowUp':
      tetroMinoDistanceY--;
      break;
    case 'ArrowRight':
      tetroMinoDistanceX++;
      break;
    case 'ArrowDown':
      tetroMinoDistanceY++;
      break;
  }
  drawPlayScreen();
};
・省略・

```

### 解説

まず、テトリミノの移動距離を変数 tetroMinoDistanceX/Y に定義します。生成時は移動していないので初期値は 0 となります。そして、矢印キーを押した時の処理を「document.onkeydown」で記述しています。これはキーボードが押される度に実行される処理となります。引数「e」には押されたキーの情報が格納されます。  
今回必要なキーは矢印キー 4 種となります。そこでスイッチ文によって押されたキー毎に処理を分岐させています。プロパティ「e.code」には押されたキーの ID 値のようなモノが入っていると思ってください。  
あとはキー毎に処理を記載するだけです。下へ移動する場合は y 軸方向へ＋ 左に移動する場合は x 軸方向に－ となります。  
画像  
これで自由にテトリミノを動かせるようになりました。しかし、仕様 ④ は「画面内でかつ他のブロックに干渉しなければ」という条件があります。・・・課題その 4  
考え方としては、移動先の画面内座標にテトリミノがある場合移動処理を無効にする というロジックが組めれば良さそうです。ただしそのロジックを組む前に、プレイ画面自体に既に動かなくなったテトリミノが存在する必要があります。なのでまずは既に動かせなくなったテトリミノを静的に書いていきましょう。

### コード

```javascript
// 画面本体
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);
};

document.onkeydown = (e) => {
  switch (e.code) {
    case 'ArrowLeft':
      tetroMinoDistanceX--;
      break;
    case 'ArrowUp':
      tetroMinoDistanceY--;
      break;
    case 'ArrowRight':
      tetroMinoDistanceX++;
      break;
    case 'ArrowDown':
      tetroMinoDistanceY++;
      break;
  }
  drawPlayScreen();
};

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

// 初期化処理
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;
  drawPlayScreen();
};
```

### 解説

考え方はテトリミノを描画したときと同じです。10×20 の長方形（=プレイ画面本体）を配列で再現し、動かせなくなったテトリミノがある位置に 1 を入れて描画していきます。ただし、テトリミノのように10×20の二次元配列をオンコードで書くのは骨が折れるので動的に作成していきます。  

まず、画面本体の描画をするうえで使う空の配列「SCREEN」を用意します。そして、初期処理で画面本体用配列を実際に作成していきます。まずはＹ軸方向です。Ｙ軸方向は 20 ブロック分なので、最大値は「PLAY_SCREEN_HEIGHT」となります。今回は 10×20 の二次元配列を作成したいため、配列 y の中身に毎回配列 x を作成していきます。その上でＸ軸方向の配列も作成していきます。初期処理段階では画面にブロックは存在していないのですべてのマスが 0 となります。  
実際の描画は drawPlayScreen で行います。画面全体を見て、配列 SCREEN の値が 1 のところだけ描画していきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3128477/66e7a998-8b87-e41d-2fce-4f242dd451e9.png)


なお、テトロミノの描画処理を 2 回実施する必要となったので、1 つの関数 drawBlock にまとめてあります。ついでにブロックがのぺっとしているので線の描画もしています。描画処理は strokeRect を使っています。引数の考え方は fillRect と同じです。  
今回はテスト用にブロックを描画したいため最後にオンコードで特定の位置に 1 を入れてブロックを描画しています。下記のような形になれば成功です。  
![画面固定.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3128477/87700d9d-a94c-4624-a5a6-c07b710d165f.png)
さて、これで動かせなくなったテトリミノ（今回はブロック）の描画ができたので、いよいよ他のブロックが干渉出来ないようにしていきます。

### コード

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

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

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;
  }
  drawPlayScreen();
};

・省略・
```

### 解説

移動確認用に canMove を作成しました。引数として xy 座標の移動分を受け取り、戻り値は boolean を返します。そして今動かそうとしているテトリミノの「今の位置」に「移動分」を加えた値（=移動後の座標位置）を変数に設定します。あとは、移動後の座標位置にブロックがあるか判定するだけです。1 ブロックでも移動後の座標にブロックがある場合は false を返します。ポイントは現在のテトリミノの情報（テトリミノの種類と座標）を基に考えるということです。  
呼び出しは移動処理の時に行います。各ボタンが押された後に移動可否判定を行い、true が戻ってきた場合のみ実際に移動させる事ができるという処理となっています。

最後に、画面外にブロックが移動できないようにしていきます。これはシンプルです。nextX/Y の値が画面の最小値と最大値を超えなければよいという形二なります。

### コード

```javascript
・省略・
const canMove = (moveX, moveY) => {
  for (let y = 0; y < TET_SIZE; y++) {
    for (let x = 0; x < TET_SIZE; x++) {
      if (tetroMino[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;
};
・省略・
```

### 解説

移動後の座標である変数 nextX/Y の値が範囲外にある時は false を返すようにしています。画面サイズは 縦 × 横 ＝ 20×10 なので、それを超えない範囲の時だけ動かすことができる というロジックです。
## まとめ

今回は、④ テトリミノは画面内でかつ他ブロックに干渉しなければ自由に動かせる を実装しました。ポイントは下記です。

1. onkeyDown を使うことでキーボードが押されたときに処理を走らせる事ができる
2. for ループを 2 回ネストすることで動的に二次元配列を作成することができる

次回は⑤ テトリミノは画面内でかつ他ブロックに干渉しなければ自由に回転できる を実装していきます。

最後にここまでのコードを記載します

```javascript
// 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, 1, 0, 0],
    [0, 1, 0, 0],
    [0, 1, 0, 0],
    [0, 1, 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) => {
  for (let y = 0; y < TET_SIZE; y++) {
    for (let x = 0; x < TET_SIZE; x++) {
      if (tetroMino[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;
};

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;
  }
  drawPlayScreen();
};

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

// 初期化処理
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;
  drawPlayScreen();
};
```