Web勉強会 vol.4 JavaScriptでテトリスを作る part.1

この記事ではChrome, Heroku, SourceTree, Atom, Node.jsを使用しています。
大学の研究室で開催している勉強会の資料共有とアーカイブのためにQiitaに投稿しています。

前回はJavaScriptの基本的な使い方を学びました。
今回はJavaScriptでテトリスを作っていきましょう。

#ゲーム盤を作る
## HTMLを作成する
まずはHTMLのtableタグを使ってゲーム盤を作りましょう。
今回は横10マス、縦20マスの盤を作成します。

```html:index.html
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>テトリス</title>
        <link rel="stylesheet" href="/stylesheets/style.css">
    </head>
    <body>
        <table id="game">
            <tr><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
            <tr><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
            <tr><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
            <tr><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
            <tr><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
            <tr><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
            <tr><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
            <tr><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
            <tr><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
            <tr><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
            <tr><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
            <tr><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
            <tr><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
            <tr><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
            <tr><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
            <tr><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
            <tr><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
            <tr><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
            <tr><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
            <tr><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
        </table>
    </body>
</html>
```

## CSSを編集する
CSSで見た目を整えていきます。

```css:style.css
body {
    background-color: #263238;
}
table {
    margin: 0 auto; /* 中央寄せ */
    width: 400px;
    height: 800px;
}
td {
    background-color: #455A64;
    width: 10%; /* 横10マス */
    height: 5%; /* 縦20マス */
}
```

これで盤は完成ですね。
![無題.jpg](https://qiita-image-store.s3.amazonaws.com/0/113969/39cc669b-c77f-b3d7-b8a6-2e00a5aa2771.jpeg)

### 色で悩んだときは
配色で悩んだ時はいつもGoogleの[Color style](https://material.google.com/style/color.html)から色を選んでいます。
Googleはいろいろなコーディング規約やスタイルガイドなどを出しているので悩んだときには参考になると思います。

## ブロック用のCSSを追加する
ブロックを示すCSSを追加してみます。

```css:style.css
.stick {
    background-color: #03A9F4;
}
```

# JavaScriptを作成する
## 基本的な構成
JavaScriptで実装するために処理と表示を分けてプログラミングを進めていきます。
そのためにまずは表示を行うような仕組みを用意します。
更に処理の部分では落下やキーボード操作などを機能ごとに関数に分けて作成していきます。

## CSSを修正する
まずはJavaScriptで表示するために先ほどのCSSを少し修正します。

```css:style.css
...
td {
    width: 10%; /* 横10マス */
    height: 5%; /* 縦20マス */
}
.default {
    background-color: #455A64;
}
.stick {
    background-color: #03A9F4;
}
```

ブロックがないマスを示すtdタグにはdefaultクラスを
ブロックがあるマスを示すtdタグにはstickクラスを割り当てます。

## JavaScriptファイルの作成と読込
public/javascriptsフォルダにmain.jsを作成します。
さらに今回は[jQuery](https://jquery.com/)を使用するためindex.htmlに読み込むためのタグを追加します。

```html:index.html
<!DOCTYPE html>
<html>
    <head>
        ...
    </head>
    <body>
        ...
        <script src="//ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js"></script>
        <script src="/javascripts/main.js" type="text/javascript"></script>
    </body>
</html>
```

jQueryはJavaScriptで作成されたとても便利なライブラリです。
クラスを追加したり中に入っているタグを検索したりするのはもちろん複雑な動きを簡単に用意することができます。

## 表示と更新
arrayという2次元配列に盤面の情報を保持します。0はブロックがない、1はブロックであることを示します。今回は実験のために一部のみ1にしてあります。
そしてそのarrayの情報をもとにtdタグのクラスを変更していくdrawという関数を追加します。
setIntervalは1秒後から実行されるのでsetIntervalとは別にdrawを呼び出しておきます。

```js:main.js
var array = [
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 1, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 1, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 1, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 1, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
];

function draw() {
    $('#game').find('tr').each(function(i, elemTr) { // trタグそれぞれに対する処理
        $(elemTr).children().each(function(j, elemTd) { // tdタグそれぞれに対する処理
            $(elemTd).removeClass(); // まずはクラスをすべてなしにする
            switch (array[i][j]) {
                case 1:
                    $(elemTd).addClass("stick"); // 1の時にはstickクラスを割り振る
                    break;
                default:
                    $(elemTd).addClass("default"); // それ以外の時にはdefaultクラスを割り振る
            }
        })
    });
}

draw(); // 読込が完了したらまず表示
setInterval(function() {
    draw();
}, 500); // 0.5秒ごとに表示を更新していきます
```

これでブロックが表示されるようになりました。
![無題.jpg](https://qiita-image-store.s3.amazonaws.com/0/113969/4a1aa405-524b-f2ec-bf6f-012c1f439fad.jpeg)

## 落下処理
ブロックが落下していく処理を追記します。

```js:main.js
...

function fall() {
    var under = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]; // 下の行にブロックがあるかどうかを保持する配列
    for (var i = 19; i >= 0; i--) {
        for (var j = 0; j < 10; j++) {
            if (under[j] == 0) {
                if (array[i][j] == 0) {
                    // 下に何もなくそのマスがブロックでもないとき
                    under[j] = 0;
                } else {
                    // 下に何もなくそのマスがブロックであるとき
                    array[i + 1][j] = array[i][j];
                    array[i][j] = 0;
                    under[j] = 0;
                }
            } else {
                if (array[i][j] == 0) {
                    // 下がブロックでそのマスがブロックでないとき
                    under[j] = 0;
                } else {
                    // 下がブロックでそのマスがブロックのとき
                    under[j] = 1;
                }
            }
        }
    }
}

draw(); // 読込が完了したらまず表示
setInterval(function() {
    fall();
    draw();
}, 500); // 0.5秒ごとに表示を更新していきます
```

これで落ちるようになりました。
fallはsetInterval内に追加することで描画する前に落下処理を行うようにしてあります。

## ブロックを呼び出す
スペースキーを押したら縦に4つならんだブロックが出てくるようにします

```js:main.js
function genBlock(blockNum) {
    switch (blockNum) {
        case 1:
            array[0][5] = blockNum;
            array[1][5] = blockNum;
            array[2][5] = blockNum;
            array[3][5] = blockNum;
            break;
    }
}

document.onkeydown = function(e) { // キーボードの処理はこのように書きます
    switch (e.code) {
        case "Space":
            genBlock(1);
            break;
    }
    draw();
}
```

## ブロックを左右に動かす
まずは現在動いているブロックがどこにあるかを保持する配列を追加します。

```js:main.js
var move = [
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 1, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 1, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 1, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 1, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
];
```

次にfall関数でブロックを移動したときにmoveも一緒に変更するようにします

```js:main.js
function fall() {
    var under = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1];
    for (var i = 19; i >= 0; i--) {
        for (var j = 0; j < 10; j++) {
            if (under[j] == 0) {
                if (array[i][j] == 0) {
                    // 下に何もなくブロックでもない
                    under[j] = 0;
                } else {
                    // 下に何もなくブロックであるとき
                    array[i + 1][j] = array[i][j];
                    array[i][j] = 0;
                    // moveも一緒に動かす
                    if (move[i][j] == 1) {
                        move[i][j] = 0;
                        move[i + 1][j] = 1;
                    }
                    under[j] = 0;
                }
            } else {
                if (array[i][j] == 0) {
                    // 下がブロックでブロックでないとき
                    under[j] = 0;
                } else {
                    // 下がブロックでブロックのとき
                    under[j] = 1;
                }
            }
        }
    }
}
```

左右に動かすときにmoveを参照しながら動かすブロックを探し、arrayもmoveも変更します。

```js:main.js
function moveBlockRight() {
    for (var i = 19; i >= 0; i--) {
        var newMove = move[i].concat();
        for (var j = 8; j >= 0; j--) {
            if (move[i][j] == 1) {
                array[i][j + 1] = array[i][j];
                array[i][j] = 0;
                newMove[j + 1] = 1;
                newMove[j] = 0;
            }
        }
        move[i] = newMove;
    }
}

function moveBlockLeft() {
    for (var i = 19; i >= 0; i--) {
        var newMove = move[i].concat();
        for (var j = 1; j < 10; j++) {
            if (move[i][j] == 1) {
                array[i][j - 1] = array[i][j];
                array[i][j] = 0;
                newMove[j - 1] = 1;
                newMove[j] = 0;
            }
        }
        move[i] = newMove;
    }
}
```

ブロックが生成されたときにもmoveが更新されるようにします。

```js:main.js
function genBlock(blockNum) {
    switch (blockNum) {
        case 1:
            array[0][5] = blockNum;
            array[1][5] = blockNum;
            array[2][5] = blockNum;
            array[3][5] = blockNum;
            // moveも一緒に変更する
            move[0][5] = 1;
            move[1][5] = 1;
            move[2][5] = 1;
            move[3][5] = 1;
            break;
    }
}
```

最後に左右の矢印キーで動くようにします。

```js:main.js
document.onkeydown = function(e) {
    switch (e.code) {
        case "Space":
            genBlock(1);
            break;
        // ここから下を追加する
        case "ArrowRight":
            moveBlockRight();
            break;
        case "ArrowLeft":
            moveBlockLeft();
            break;
    }
    draw();
}
```

これで左右には動くようになりました。
JavaScriptでは変数に変数を代入するとき参照になってしまうので実態で渡さないとどちらの変数も書き換わってしまう。
壁にぶつかったように見えるのは単純に端の縦1列ずつに関して移動をしないようにしているだけです。

## 一度に一つのブロックだけがでるようにする
このままではいくらでもブロックを呼び出せてしましい動きがおかしいので1度に一つのブロックのみ出して動かすことができるようにします。

動かす対象があるかどうかを保持しする変数を用意します。

```js:main.js
var moveFlag = 0; // 動かす対象があるかどうか(0はない、1はあることを示す)
```

動かす対象があればブロックは追加しないようにします。

```js:main.js
function genBlock(blockNum) {
    if (moveFlag == 0) {
        switch (blockNum) {
            case 1:
                array[0][5] = blockNum;
                array[1][5] = blockNum;
                array[2][5] = blockNum;
                array[3][5] = blockNum;
                move[0][5] = 1;
                move[1][5] = 1;
                move[2][5] = 1;
                move[3][5] = 1;
                break;
        }
        moveFlag = 1;
    }
}
```

動かす対象のブロックが動けなくなったときには動かす対象がなくなったことにします。

```js:main.js
function fall() {
    var under = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1];
    for (var i = 19; i >= 0; i--) {
        for (var j = 0; j < 10; j++) {
            if (under[j] == 0) {
                if (array[i][j] == 0) {
                    ...
                } else {
                    ...
                }
            } else {
                if (array[i][j] == 0) {
                    ...
                } else {
                    // 下がブロックでブロックのとき
                    if (move[i][j] == 1) {
                        resetMove();
                    }
                    under[j] = 1;
                }
            }
        }
    }
}

function resetMove() {
    moveFlag = 0;
    for (var i = 0; i < 20; i++) {
        for (var j = 0; j < 10; j++) {
            move[i][j] = 0;
        }
    }
}
```

これでゲームっぽくなってきました。

## 揃った列を消す
最後に横１列が揃った時に消えるようにします。

```js:main.js
function checkDelete() {
    for (var i = 19; i >= 0; i--) {
        if (!array[i].includes(0)) {
            for (var j = 0; j < 10; j++) {
                array[i][j] = 0;
            }
        }
    }
}

...

setInterval(function() {
    checkDelete(); // ここを追加
    fall();
    draw();
}, 500);
```

JavaScriptではincludes()という関数でその配列に対象が含まれるか確認できます。

# チャレンジ

- ブロックの種類を増やしてみる
    - CSSの追加
    - genBlockにパターンをふやす
    - 呼び出すキーを割り当てる
    - 左右に飛び出ないようにする