テトリスを作ろう3

JavaScript

では前回の続きを作っていこう。

落下後、ボードに書き込む

 落下したテトリミノをボード配列に書き込む処理を以下のように追記する。

      //動きが止まったtetをボード座標に書き写す
      const fixTet = () => {
        for (let y = 0; y < tetSize; y++) {
          for (let x = 0; x < tetSize; x++) {
            if (tet[y][x]) {
              //ボードに書き込む
              board[offsetY + y][offsetX + x] = 1;
            }
          }
        }
      };
      //繰り返し行われる落下処理
      const dropTet = () => {
        //下に行けたら
        if (canMove(0, 1)) {
          //下に行く
          offsetY++;
        } else {
          //行けなかったら固定する
          fixTet();
          //初期位置に戻す
          initStartPos();
        }
        draw();
      };

いざ、実行

ボードに書き込む処理とスタート地点に戻す処理によりグッと完成に近づいてきた。

揃ったラインを消す

列も揃えられるようになったので揃ったラインを消す処理を作成しよう。以下のように追記する。

      const clearLine = () => {
        //ボードの行を上から調査
        for (let y = 0; y < boardRow; y++) {
          //一列揃ってると仮定する(フラグ)
          let isLineOK = true;
          //列に0が入っていないか調査
          for (let x = 0; x < boardCol; x++) {
            if (board[y][x]===0) {
              //0が入ってたのでフラグをfalse
              isLineOK = false;
              break;
            }
          }
          if (isLineOK) {//ここに来るということはその列が揃っていたことを意味する
            //その行から上に向かってfor文を動かす
            for (let ny = y; ny > 0; ny--) {
              for (let nx = 0; nx < boardCol; nx++) {
                //一列上の情報をコピーする
                board[ny][nx] = board[ny - 1][nx];
              }
            }
          }
        }
      };
      //繰り返し行われる落下処理
      const dropTet = () => {
        //下に行けたら
        if (canMove(0, 1)) {
          //下に行く
          offsetY++;
        } else {
          //行けなかったら固定する
          fixTet();
          //揃ったラインがあったら消す
          clearLine();
          //初期位置に戻す
          initStartPos();
        }
        draw();
      };

いざ実行!

実行してみよう。揃った行が削除されることがわかる。現状同時に消すことができるのは2列までだが、それもできることも確認しよう。

ポイント解説

詳細にコメントをつけたのでしっかり読解してほしい。基本は揃った行に対して上の一列の情報をコピーすることだが、その際、揃った行から上に向かって処理をしていくことが大事だ。

7種類のブロックが出るようにする

現状1種類しか出ないので、7種類でるようにしよう。まずは以下のように配列を追記する。その際、もともとあったtet配列は削除し、変数だけ残す

      //tetの1辺の大きさ
      const tetSize = 4;
      //テトリミノの種類
      const tetTypes = [
        [], //最初の要素を空としておく
        [
          [0, 0, 0, 0],
          [0, 1, 1, 0],
          [0, 1, 1, 0],
          [0, 0, 0, 0],
        ],
        [
          [0, 0, 0, 0],
          [0, 1, 0, 0],
          [1, 1, 1, 0],
          [0, 0, 0, 0],
        ],
        [
          [0, 0, 0, 0],
          [1, 1, 0, 0],
          [0, 1, 1, 0],
          [0, 0, 0, 0],
        ],
        [
          [0, 0, 0, 0],
          [0, 0, 1, 1],
          [0, 1, 1, 0],
          [0, 0, 0, 0],
        ],
        [
          [0, 0, 0, 0],
          [1, 1, 1, 1],
          [0, 0, 0, 0],
          [0, 0, 0, 0],
        ],
        [
          [0, 0, 0, 0],
          [1, 1, 1, 0],
          [0, 0, 1, 0],
          [0, 0, 0, 0],
        ],
        [
          [0, 0, 0, 0],
          [0, 0, 1, 0],
          [1, 1, 1, 0],
          [0, 0, 0, 0],
        ],
      ];
      //テトリミノのindex
      let tet_idx;
      //選択されたtet
      let tet;

      //テトリミノのオフセット量(何マス分ずれているか)
      let offsetX = 0;
      let offsetY = 0;

 ランダムにtetを選択

 実際に7種類から選ぶ処理を以下のように追記する。

      //繰り返し行われる落下処理
      const dropTet = () => {
        //下に行けたら
        if (canMove(0, 1)) {
          //下に行く
          offsetY++;
        } else {
          //行けなかったら固定する
          fixTet();
          //揃ったラインがあったら消す
          clearLine();
          //抽選
          tet_idx = randomIdx();
          tet = tetTypes[tet_idx];
          //初期位置に戻す
          initStartPos();
        }
        draw();
      };
      const initStartPos = () => {
        offsetX = boardCol / 2 - tetSize / 2;
        offsetY = 0;
      };

      //テトリミノのindexを抽選
      const randomIdx = () => {
        return Math.floor(Math.random() * (tetTypes.length - 1)) + 1;
      };

      //初期化処理
      const init = () => {
        //ボード(20*10を0埋め)
        for (let y = 0; y < boardRow; y++) {
          board[y] = [];
          for (let x = 0; x < boardCol; x++) {
            board[y][x] = 0;
          }
        }
        //テスト用
        //board[3][5]=1;

        //最初のテトリミノを抽選
        tet_idx = randomIdx();
        tet = tetTypes[tet_idx];

        initStartPos();
        //繰り返し処理
        timerId=setInterval(dropTet,speed);

        draw();
      };

実行

7種類のテトリミノがランダムに出てくるようになった!

ブロックに色をつける

ブロックに色をつけていこう。まずは色の配列を追加する。

     //テトリミノの種類
      const tetTypes = [
        [], //0を空としておく
        [
          [0, 0, 0, 0],
          [0, 1, 1, 0],
          [0, 1, 1, 0],
          [0, 0, 0, 0],
        ],
        略
        [
          [0, 0, 0, 0],
          [0, 0, 1, 0],
          [1, 1, 1, 0],
          [0, 0, 0, 0],
        ],
      ];

      //テトリミノの色
      const tetColors = [
        '',//これが選択されることはない
        '#f6fe85',
        '#07e0e7',
        '#7ced77',
        '#f78ff0',
        '#f94246',
        '#9693fe',
        '#f2b907',
      ];

以下ハイライト部分を修正する。

       //描画処理
      const draw = () => {
        //塗りに黒を設定
        ctx.fillStyle = '#000';
        //キャンバスを塗りつぶす
        ctx.fillRect(0, 0, canvasW, canvasH);

        //ボードに存在しているブロックを塗る
        for (let y = 0; y < boardRow; y++) {
          for (let x = 0; x < boardCol; x++) {
            if (board[y][x]) {
              drawBlock(x, y,board[y][x]);
            }
          }
        }

        //テトリミノの描画
        for (let y = 0; y < tetSize; y++) {
          for (let x = 0; x < tetSize; x++) {
            if (tet[y][x]) {
              drawBlock(offsetX + x, offsetY + y,tet_idx);
            }
          }
        }
      };
      //ブロック一つを描画する
      const drawBlock = (x, y,tet_idx) => {
        let px = x * blockSize;
        let py = y * blockSize;
        //塗りを設定
        ctx.fillStyle = tetColors[tet_idx];
        ctx.fillRect(px, py, blockSize, blockSize);
        //線を設定
        ctx.strokeStyle = 'black';
        //線を描画
        ctx.strokeRect(px, py, blockSize, blockSize);
      };

      //指定された方向に移動できるか?(x移動量,y移動量,対象tet)
      const canMove = (dx, dy, nowTet = tet) => {
        for (let y = 0; y < tetSize; y++) {
          for (let x = 0; x < tetSize; x++) {
            //その場所にブロックがあれば
            if (nowTet[y][x]) {
              //ボード座標に変換
              let nx = offsetX + x + dx;
              let ny = offsetY + y + dy;
              if (
                //調査する座標がボード外だったらできない
                ny < 0 ||
                nx < 0 ||
                ny >= boardRow ||
                nx >= boardCol ||
                //移動したいボード上の場所にすでに存在してたらできない
                board[ny][nx]
              ) {
                //移動できない
                return false;
              }
            }
          }
        }
        //移動できる
        return true;
      };
      //回転
      const createRotateTet = () => {
        //新しいtetを作る
        let newTet = [];
        for (let y = 0; y < tetSize; y++) {
          newTet[y] = [];
          for (let x = 0; x < tetSize; x++) {
            //時計回りに90度回転させる
            newTet[y][x] = tet[tetSize - 1 - x][y];
          }
        }
        return newTet;
      };

      document.onkeydown = (e) => {
        switch (e.keyCode) {
          case 37: //左
            if (canMove(-1, 0)) offsetX--;
            break;
          case 38: //上
            if (canMove(0, -1)) offsetY--;
            break;
          case 39: //右
            if (canMove(1, 0)) offsetX++;
            break;
          case 40: //下
            if (canMove(0, 1)) offsetY++;
            break;
          case 32: //space
            let newTet = createRotateTet();
            if (canMove(0, 0, newTet)) {
              tet = newTet;
            }
        }
        draw();
      };
      //動きが止まったtetをボード座標に書き写す
      const fixTet = () => {
        for (let y = 0; y < tetSize; y++) {
          for (let x = 0; x < tetSize; x++) {
            if (tet[y][x]) {
              //ボードに書き込む
              board[offsetY + y][offsetX + x] = tet_idx;
            }
          }
        }
      };

いざ実行!

色が加わってよりテトリスらしくなった。

ポイント

配列やカラー配列の最初の要素を空として,index抽選の際も0が選ばれないようにしているが、これはボード配列に格納する際にtet_idxを格納するためだ。
(上記ソース108行目)
tet_idxに0があると何もないを表す0と区別がつかなくなってしまう。

終了処理

最後にゲームオーバー処理をつくってみよう。以下ハイライト部分を追記

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

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

      //描画処理
      const draw = () => {
        //塗りに黒を設定
        ctx.fillStyle = '#000';
        //キャンバスを塗りつぶす
        ctx.fillRect(0, 0, canvasW, canvasH);

        //ボードに存在しているブロックを塗る
        for (let y = 0; y < boardRow; y++) {
          for (let x = 0; x < boardCol; x++) {
            if (board[y][x]) {
              drawBlock(x, y,board[y][x]);
            }
          }
        }

        //テトリミノの描画
        for (let y = 0; y < tetSize; y++) {
          for (let x = 0; x < tetSize; x++) {
            if (tet[y][x]) {
              drawBlock(offsetX + x, offsetY + y,tet_idx);
            }
          }
        }
        if (isGameOver) {
          const s = 'GAME OVER';
          ctx.font = "40px 'MS ゴシック'";
          const w = ctx.measureText(s).width;
          const x = canvasW / 2 - w / 2;
          const y = canvasH / 2 - 20;
          ctx.fillStyle = 'white';
          ctx.fillText(s, x, y);
        }
      };
      //ブロック一つを描画する
      const drawBlock = (x, y,tet_idx) => {
        let px = x * blockSize;
        let py = y * blockSize;
        //塗りを設定
        ctx.fillStyle = tetColors[tet_idx];
        ctx.fillRect(px, py, blockSize, blockSize);
        //線を設定
        ctx.strokeStyle = 'black';
        //線を描画
        ctx.strokeRect(px, py, blockSize, blockSize);
      };

      //指定された方向に移動できるか?(x移動量,y移動量,対象tet)
      const canMove = (dx, dy, nowTet = tet) => {
        for (let y = 0; y < tetSize; y++) {
          for (let x = 0; x < tetSize; x++) {
            //その場所にブロックがあれば
            if (nowTet[y][x]) {
              //ボード座標に変換(offsetX(-2~8)+x(0~3)+移動量(-1~1)
              let nx = offsetX + x + dx;
              let ny = offsetY + y + dy;
              if (
                //調査する座標がボード外だったらできない
                ny < 0 ||
                nx < 0 ||
                ny >= boardRow ||
                nx >= boardCol ||
                //移動したいボード上の場所にすでに存在してたらできない
                board[ny][nx]
              ) {
                //移動できない
                return false;
              }
            }
          }
        }
        //移動できる
        return true;
      };
      //回転
      const createRotateTet = () => {
        //新しいtetを作る
        let newTet = [];
        for (let y = 0; y < tetSize; y++) {
          newTet[y] = [];
          for (let x = 0; x < tetSize; x++) {
            //時計回りに90度回転させる
            newTet[y][x] = tet[tetSize - 1 - x][y];
          }
        }
        return newTet;
      };

      document.onkeydown = (e) => {
        if (isGameOver) return;
        switch (e.keyCode) {
          case 37: //左
            if (canMove(-1, 0)) offsetX--;
            break;
          case 38: //上
            if (canMove(0, -1)) offsetY--;
            break;
          case 39: //右
            if (canMove(1, 0)) offsetX++;
            break;
          case 40: //下
            if (canMove(0, 1)) offsetY++;
            break;
          case 32: //space
            let newTet = createRotateTet();
            if (canMove(0, 0, newTet)) {
              tet = newTet;
            }
        }
        draw();
      };
      //動きが止まったtetをボード座標に書き写す
      const fixTet = () => {
        for (let y = 0; y < tetSize; y++) {
          for (let x = 0; x < tetSize; x++) {
            if (tet[y][x]) {
              //ボードに書き込む
              board[offsetY + y][offsetX + x] = tet_idx;
            }
          }
        }
      };
      const clearLine = () => {
        //ボードの行を上から調査
        for (let y = 0; y < boardRow; y++) {
          //一列揃ってると仮定する(フラグ)
          let isLineOK = true;
          //列に0が入っていないか調査
          for (let x = 0; x < boardCol; x++) {
            if (board[y][x]===0) {
              //0が入ってたのでフラグをfalse
              isLineOK = false;
              break;
            }
          }
          if (isLineOK) {//ここに来るということはその列が揃っていたことを意味する
            //その行から上に向かってfor文を動かす
            for (let ny = y; ny > 0; ny--) {
              for (let nx = 0; nx < boardCol; nx++) {
                //一列上の情報をコピーする
                board[ny][nx] = board[ny - 1][nx];
              }
            }
          }
        }
      };
      //繰り返し行われる落下処理
      const dropTet = () => {
        if (isGameOver) return;
        //下に行けたら
        if (canMove(0, 1)) {
          //下に行く
          offsetY++;
        } else {
          //行けなかったら固定する
          fixTet();
          //揃ったラインがあったら消す
          clearLine();
          //抽選
          tet_idx = randomIdx();
          tet = tetTypes[tet_idx];
          //初期位置に戻す
          initStartPos();
          //次のtetを出せなかったらGameOver
          if (!canMove(0, 0)) {
            isGameOver = true;
            clearInterval(timerId);
          }
        }
        draw();
      };

ポイント解説

今回は次のを出せなくなったときにゲームオーバーとした。setIntervalで繰り返し行われている処理はclearIntervalメソッドにtimerIdを渡すと止めることができる。

完成!!

全3回に渡ってテトリスを作成したが、理解することができたであろうか?
点数の概念やスピードアップの仕組みなどは未実装だ。ぜひ色々カスタマイズしてもらいたい。

全ソース

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>テトリス</title>
    <style>
      body {
        background: #ddf5ff;
      }
      #container {
        margin: 0 auto;
      }
    </style>
  </head>
  <body onload="init()">
    <div id="container">
      <canvas id="cvs"></canvas>
    </div>
    <script>
      //落下サイクル(小さい方が速い)
      const speed = 300;
      //ブロック1マスの大きさ
      const blockSize = 30;
      //ボードサイズ
      const boardRow = 20;
      const boardCol = 10;
      //キャンバスの取得
      const cvs = document.getElementById('cvs');
      //2dコンテキストを取得
      const ctx = cvs.getContext('2d');
      //キャンバスサイズ
      const canvasW = blockSize * boardCol;
      const canvasH = blockSize * boardRow;
      cvs.width = canvasW;
      cvs.height = canvasH;
      //コンテナの設定
      const container = document.getElementById('container');
      container.style.width = canvasW + 'px';

      //tetの1辺の大きさ
      const tetSize = 4;
      //テトリミノの種類
      const tetTypes = [
        [], //0を空としておく
        [
          [0, 0, 0, 0],
          [0, 1, 1, 0],
          [0, 1, 1, 0],
          [0, 0, 0, 0],
        ],
        [
          [0, 0, 0, 0],
          [0, 1, 0, 0],
          [1, 1, 1, 0],
          [0, 0, 0, 0],
        ],
        [
          [0, 0, 0, 0],
          [1, 1, 0, 0],
          [0, 1, 1, 0],
          [0, 0, 0, 0],
        ],
        [
          [0, 0, 0, 0],
          [0, 0, 1, 1],
          [0, 1, 1, 0],
          [0, 0, 0, 0],
        ],
        [
          [0, 0, 0, 0],
          [1, 1, 1, 1],
          [0, 0, 0, 0],
          [0, 0, 0, 0],
        ],
        [
          [0, 0, 0, 0],
          [1, 1, 1, 0],
          [0, 0, 1, 0],
          [0, 0, 0, 0],
        ],
        [
          [0, 0, 0, 0],
          [0, 0, 1, 0],
          [1, 1, 1, 0],
          [0, 0, 0, 0],
        ],
      ];

      //テトリミノの色
      const tetColors = [
        '',//これが選択されることはない
        '#f6fe85',
        '#07e0e7',
        '#7ced77',
        '#f78ff0',
        '#f94246',
        '#9693fe',
        '#f2b907',
      ];

      //テトリミノのindex
      let tet_idx;
      //選択されたtet
      let tet;

      //テトリミノのオフセット量(何マス分ずれているか)
      let offsetX = 0;
      let offsetY = 0;

      //ボード本体
      const board = [];

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

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

      //描画処理
      const draw = () => {
        //塗りに黒を設定
        ctx.fillStyle = '#000';
        //キャンバスを塗りつぶす
        ctx.fillRect(0, 0, canvasW, canvasH);

        //ボードに存在しているブロックを塗る
        for (let y = 0; y < boardRow; y++) {
          for (let x = 0; x < boardCol; x++) {
            if (board[y][x]) {
              drawBlock(x, y,board[y][x]);
            }
          }
        }

        //テトリミノの描画
        for (let y = 0; y < tetSize; y++) {
          for (let x = 0; x < tetSize; x++) {
            if (tet[y][x]) {
              drawBlock(offsetX + x, offsetY + y,tet_idx);
            }
          }
        }
        if (isGameOver) {
          const s = 'GAME OVER';
          ctx.font = "40px 'MS ゴシック'";
          const w = ctx.measureText(s).width;
          const x = canvasW / 2 - w / 2;
          const y = canvasH / 2 - 20;
          ctx.fillStyle = 'white';
          ctx.fillText(s, x, y);
        }
      };
      //ブロック一つを描画する
      const drawBlock = (x, y,tet_idx) => {
        let px = x * blockSize;
        let py = y * blockSize;
        //塗りを設定
        ctx.fillStyle = tetColors[tet_idx];
        ctx.fillRect(px, py, blockSize, blockSize);
        //線を設定
        ctx.strokeStyle = 'black';
        //線を描画
        ctx.strokeRect(px, py, blockSize, blockSize);
      };

      //指定された方向に移動できるか?(x移動量,y移動量,対象tet)
      const canMove = (dx, dy, nowTet = tet) => {
        for (let y = 0; y < tetSize; y++) {
          for (let x = 0; x < tetSize; x++) {
            //その場所にブロックがあれば
            if (nowTet[y][x]) {
              //ボード座標に変換(offsetX(-2~8)+x(0~3)+移動量(-1~1)
              let nx = offsetX + x + dx;
              let ny = offsetY + y + dy;
              if (
                //調査する座標がボード外だったらできない
                ny < 0 ||
                nx < 0 ||
                ny >= boardRow ||
                nx >= boardCol ||
                //移動したいボード上の場所にすでに存在してたらできない
                board[ny][nx]
              ) {
                //移動できない
                return false;
              }
            }
          }
        }
        //移動できる
        return true;
      };
      //回転
      const createRotateTet = () => {
        //新しいtetを作る
        let newTet = [];
        for (let y = 0; y < tetSize; y++) {
          newTet[y] = [];
          for (let x = 0; x < tetSize; x++) {
            //時計回りに90度回転させる
            newTet[y][x] = tet[tetSize - 1 - x][y];
          }
        }
        return newTet;
      };

      document.onkeydown = (e) => {
        if (isGameOver) return;
        switch (e.keyCode) {
          case 37: //左
            if (canMove(-1, 0)) offsetX--;
            break;
          case 38: //上
            if (canMove(0, -1)) offsetY--;
            break;
          case 39: //右
            if (canMove(1, 0)) offsetX++;
            break;
          case 40: //下
            if (canMove(0, 1)) offsetY++;
            break;
          case 32: //space
            let newTet = createRotateTet();
            if (canMove(0, 0, newTet)) {
              tet = newTet;
            }
        }
        draw();
      };
      //動きが止まったtetをボード座標に書き写す
      const fixTet = () => {
        for (let y = 0; y < tetSize; y++) {
          for (let x = 0; x < tetSize; x++) {
            if (tet[y][x]) {
              //ボードに書き込む
              board[offsetY + y][offsetX + x] = tet_idx;
            }
          }
        }
      };
      const clearLine = () => {
        //ボードの行を上から調査
        for (let y = 0; y < boardRow; y++) {
          //一列揃ってると仮定する(フラグ)
          let isLineOK = true;
          //列に0が入っていないか調査
          for (let x = 0; x < boardCol; x++) {
            if (board[y][x]===0) {
              //0が入ってたのでフラグをfalse
              isLineOK = false;
              break;
            }
          }
          if (isLineOK) {//ここに来るということはその列が揃っていたことを意味する
            //その行から上に向かってfor文を動かす
            for (let ny = y; ny > 0; ny--) {
              for (let nx = 0; nx < boardCol; nx++) {
                //一列上の情報をコピーする
                board[ny][nx] = board[ny - 1][nx];
              }
            }
          }
        }
      };
      //繰り返し行われる落下処理
      const dropTet = () => {
        if (isGameOver) return;
        //下に行けたら
        if (canMove(0, 1)) {
          //下に行く
          offsetY++;
        } else {
          //行けなかったら固定する
          fixTet();
          //揃ったラインがあったら消す
          clearLine();
          //抽選
          tet_idx = randomIdx();
          tet = tetTypes[tet_idx];
          //初期位置に戻す
          initStartPos();
          //次のtetを出せなかったらGameOver
          if (!canMove(0, 0)) {
            isGameOver = true;
            clearInterval(timerId);
          }
        }
        draw();
      };
      const initStartPos = () => {
        offsetX = boardCol / 2 - tetSize / 2;
        offsetY = 0;
      };

      //テトリミノのindexを抽選
      const randomIdx = () => {
        return Math.floor(Math.random() * (tetTypes.length - 1)) + 1;
      };

      //初期化処理
      const init = () => {
        //ボード(20*10を0埋め)
        for (let y = 0; y < boardRow; y++) {
          board[y] = [];
          for (let x = 0; x < boardCol; x++) {
            board[y][x] = 0;
          }
        }
        //テスト用
        //board[3][5]=1;

        //最初のテトリミノを抽選
        tet_idx = randomIdx();
        tet = tetTypes[tet_idx];

        initStartPos();
        //繰り返し処理
        timerId=setInterval(dropTet,speed);

        draw();
      };
    </script>
  </body>
</html>

コメント

タイトルとURLをコピーしました