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

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

JavaScript でライブラリを使わずにテトリス作成してみました。世の中にテトリス開発記事はたくさんありました。が、理解するのに非常に苦労したポイントも多くありました。そこでアウトプットを兼ねてできるだけ丁寧に解説付きで記載してみました。  
下記参考にしたサイトになります。
https://joytas.net/programming/tetris1 （本記事のベースにさせてもらっています。ある程度JS触れる方はこちらのサイト見ていただいたほうが早く進められるかと思います）
https://www.youtube.com/watch?v=LJlKaTwtSdI

## はじめに

各回につき 1 回は課題 N という形で表現している箇所があります。その個所に来たら次に進むのをストップして一度どうしたらいいのか 5 分ほど考えてみましょう。  
また、コーディングに正解はないので自分なりのロジックが浮かんだら記事内のコードと比較してどちらがより合理的か検討してみてください。

## テトリスの仕様を知る

まずはテトリスの仕様から抑えましょう。  
① 画面サイズは縦 20 ブロック分、横 10 ブロック分である
② 4 つの正方形ブロックで構成されるテトリミノがある
③ テトリミノは 7 種類ある
④ テトリミノは画面内でかつ他ブロックに干渉しなければ自由に動かせる
⑤ テトリミノは画面内でかつ他ブロックに干渉しなければ自由に回転できる
⑥ テトリミノは画面最下部または他ブロックの上部に面したとき動かせなくなる
⑦ テトリミノは画面上部に生成される
⑧ テトリミノは一定間隔で下方向に移動する
⑨ テトリミノはランダムに生成される
⑩ 横 10 ブロック分がそろった場合、そろった列は消えて上の列が下りてくる
⑪ テトリミノ生成場所にブロックがある場合、ゲームが終了する

## 画面を作ろう

まずはベースとなる画面を作りましょう。（仕様 ① を実装する）

### コード

```HTML
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>テトリス</title>
    <style>
      body {
        background: #888;
      }
      #container {
        margin: 0 auto;
      }
    </style>
  </head>
  <body onload="init()">
    <div id="container">
      <canvas id="canvas"></canvas>
    </div>
    <script src="Tet.js"></script>
  </body>
</html>
```

### 解説

今回は HTML と CSS は同じファイル、JS は別ファイルで作成します。  
body 要素の背景色はお好みのもので大丈夫です。今回は RGB カラーコードで指定しました。「onload="init()"」は html を読み込んだ際に一番最初に実行する処理（＝初期処理）となり、JS 側で記述します。  
div 要素 container は、web 画面開発おける慣習みたいなものだと思ってください。[詳しい事はコチラ](https://snownotes.org/advantage-of-container/)  
最後に canvas 要素についてです。canvas とは簡単にいうと「JS を使うことで動的にお絵描きできるようになる」やつです。  
ここまで書けたら実際に html ファイルを開いてみましょう。スクリーン全体の背景色が変更されているかと思います。

次に、テトリスのプレイ画面を書きましょう

### コード

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

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

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

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

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

// 初期化処理
const init = () => {
  drawPlayScreen();
};
```

### 解説

はじめに「document.getElementById('canvas')」を使って HTML 要素を取得しています。これで動的に HTML に変更を加えられるようになりました。次に、「CANVAS.getContext('2d');」を使って 2 次元図形を扱うことを宣言しています。ちなみに 3 次元図形を扱うには「3d」にしてさらに「webgl」たるものを利用する必要があります。  
プロパティ「CANVAS_2D.fillStyle」を使って、画面の背景色を指定し、「fillRect」を使って塗りつぶしを行えば完了です。  
「fillRect」の引数は一番左から「塗りつぶし開始地点の x 座標」「塗りつぶし開始地点の y 座標」「x 方向への塗りつぶし範囲」「y 方向への塗りつぶし範囲」となっています。  
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3128477/ea877e16-40d6-e1f4-df7a-e53b691bb567.png)


最後に、作成した drawPlayScreen を実行するための初期化処理を実装します。html 側の onload にて記載した Init()を作成して記述します。

さて、実際に実行してみると画面がかなり小さいです。・・・ **課題その 1**  
というのも、「CANVAS.width/height」や「CANVAS_2D.fillRect」の引数は単位指定しない場合 px 換算されてしまうからです。  
そんなわけでプレイ画面を大きくする必要があります。あなたならどうやって「課題その 1」を解決しますか？

塗りつぶし範囲を広げたらいいんだから「CANVAS_WIDTH」と「CANVAS_HEIGHT 」の値をテキトーに広げたら解決  
と考えた人は惜しいです。というのも、仕様 ③ を思い出しましょう。  
仕様 ①：画面サイズは縦 20 ブロック分、横 10 ブロック分である

テキトーに画面サイズを設定してしまうと、ブロックの大きさを合わせるのが大変です。そこで逆の視点を持ちましょう。ブロックのサイズの比率に合わせればよいと。

### コード

```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 ブロックのサイズを決めました。そのうえで「キャンバス倍率」を新しく宣言し、ここに仕様 ① の要件をあてこみます。  
実際のプレイ画面サイズは「ブロックサイズ × 画面サイズ倍率」によって表現しています。  
実際に実行してみるといい感じの画面サイズになったかと思います。  
さいごに画面が左端にあるのが気になるので、真ん中に寄せます。

### コード

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

// 初期化処理
const init = () => {
  drawPlayScreen();
};
```

### 解説

「document.getElementById」を使って container を取得します。そのあと、「container」の「width」（横幅）を真ん中に来るように調整しています。div 要素は width の値を指定しなかった場合、「auto」がデフォルトで入るようになっています。auto は親要素の幅に依存するため、container の横幅も同様に画面いっぱいになっています。さらに container 内部の canvas 要素は親要素の左端に描画されます。そのため、canvas の親要素である container の横幅を 300px（＝プレイ画面幅と同じ）にすることで真ん中に描画される という仕組みになっています。  
下記画面のような描画がされていたら成功です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3128477/f61dbf2e-c51a-e179-3df0-bf7c0299fdb5.png)


## まとめ

さて、今回は仕様 ① を実装してみました。抑えるポイントは下記です。

1. canvas を使うことで動的に描画ができる
2. HTML の要素を JS で取得するには documetn.getElementById を使用する  
  
[その2はこちら](https://qiita.com/namakoRice/items/4fefc6557af480d8e9a5)

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

### コード

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

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

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

// 初期化処理
const init = () => {
  drawPlayScreen();
};
```