PR

7日目 揃ったラインを消す処理【JavaScriptでテトリス完成】

ソースコード

はじめに

スマホだけを使って、一週間でテトリスを作っていきます。

1日目 キャンバスの設定と描写
2日目 フィールドの設定と描写
3日目 テトリミノの設定と描写
4日目 テトリミノの落下と回転
5日目 スマホでの操作と画面固定
6日目 テトリミノの当たり判定
▶︎7日目 揃ったラインを消す処理

パソコンがなくても、スマホだけでwebアプリケーションの開発ができます。

プログラミングに興味がある人やプログラミングの勉強を始めたばかりの人に向けて、一からわかりやすく解説していきます。

7日目でやること

前回は、テトリミノが横の壁や下の床に当たったとき、壁や床を超えて移動できないようにするところまでやりました。

今回は、落下したテトリミノをフィールド情報に記憶させて、ブロックが横一列に揃ったらラインを消す処理をやって行きます。

7日目 横一列に揃ったラインを消す処理

全体のソースコード

まずは完成したソースコードをご覧ください。

HTMLのコード

<!DOCTYPE html>
<html lang="ja">

<head>
  <meta charset="utf-8">
  <link rel="stylesheet" href="style.css">
  <title>テトリス</title>
</head>

<body>
  <div class="main">
    <canvas id="canvas01"></canvas>
  </div>
  <script src="script.js"></script>
</body>

</html>

CSSのコード

html {
  height: 100%;
}

body {
  height: 100%;
  margin: 0;
}

.main {
  height: 100%;
  text-align: center;
}

#canvas01 {
  height: calc(100% - 8px);
}

JavaScriptのコード

// ============ 変数 ============

// フィールドサイズ
const FIELD_X = 10   // 横のマスの数
const FIELD_Y = 20   // 縦のマスの数
const MINO_SIZE = 30 // マスの大きさ

// テトリミノが落ちる速度
const DROP_SPEED = 500;

// キャンバスの設定
let canvas = document.getElementById("canvas01")
let conText = canvas.getContext("2d")
canvas.width = MINO_SIZE * FIELD_X
canvas.height = MINO_SIZE * FIELD_Y
canvas.style.border = "4px solid #050505"

// 色の設定
const COLOR = [
  "#F2F2F2",   // フィールド背景
  "#C8C8C8",   // 枠の線
  "#00F2F2",   // Iミノ 水色
  "#F2F200",   // Oミノ 黄色
  "#00F200",   // Sミノ 黄緑
  "#F20000",   // Zミノ 赤
  "#0000F2",   // Jミノ 青
  "#F2A200",   // Lミノ オレンジ
  "#F200F2"    // Tミノ 紫
]

// フィールドの初期設定
let field = []
for (let i = 0; i < FIELD_Y; i++) {
  field[i] = []
  for (let j = 0; j < FIELD_X; j++) {
    field[i][j] = 0
  }
}

// テトリミノの初期設定
let mino_x = Math.ceil((FIELD_X / 2) - 2)
let mino_y = 0

let mino_type = Math.trunc(Math.random() * 7)
let mino_angle = 0

let minoshapes = [
// mino_type_I
  // mino_angle_0
  [[
    [0,2,0,0],
    [0,2,0,0],
    [0,2,0,0],
    [0,2,0,0]
  ],
  // mino_angle_90
  [
    [0,0,0,0],
    [0,0,0,0],
    [2,2,2,2],
    [0,0,0,0]
  ],
  // mino_angle_180
  [
    [0,2,0,0],
    [0,2,0,0],
    [0,2,0,0],
    [0,2,0,0]
  ],
  // mino_angle_270
  [
    [0,0,0,0],
    [0,0,0,0],
    [2,2,2,2],
    [0,0,0,0]
  ]],
// mino_type_O
  // mino_angle_0
  [[
    [0,3,3,0],
    [0,3,3,0],
    [0,0,0,0],
    [0,0,0,0]
  ],
  // mino_angle_90
  [
    [0,3,3,0],
    [0,3,3,0],
    [0,0,0,0],
    [0,0,0,0]
  ],
  // mino_angle_180
  [
    [0,3,3,0],
    [0,3,3,0],
    [0,0,0,0],
    [0,0,0,0]
  ],
  // mino_angle_270
  [
    [0,3,3,0],
    [0,3,3,0],
    [0,0,0,0],
    [0,0,0,0]
  ]],
// mino_type_S
  // mino_angle_0
  [[
    [0,4,4,0],
    [4,4,0,0],
    [0,0,0,0],
    [0,0,0,0]
  ],
  // mino_angle_90
  [
    [0,4,0,0],
    [0,4,4,0],
    [0,0,4,0],
    [0,0,0,0]
  ],
  // mino_angle_180
  [
    [0,4,4,0],
    [4,4,0,0],
    [0,0,0,0],
    [0,0,0,0]
  ],
  // mino_angle_270
  [
    [0,4,0,0],
    [0,4,4,0],
    [0,0,4,0],
    [0,0,0,0]
  ]],
// mino_type_Z
  // mino_angle_0
  [[
    [5,5,0,0],
    [0,5,5,0],
    [0,0,0,0],
    [0,0,0,0]
  ],
  // mino_angle_90
  [
    [0,0,5,0],
    [0,5,5,0],
    [0,5,0,0],
    [0,0,0,0]
  ],
  // mino_angle_180
  [
    [5,5,0,0],
    [0,5,5,0],
    [0,0,0,0],
    [0,0,0,0]
  ],
  // mino_angle_270
  [
    [0,0,5,0],
    [0,5,5,0],
    [0,5,0,0],
    [0,0,0,0]
  ]],
// mino_type_J
  // mino_angle_0
  [[
    [0,0,6,0],
    [0,0,6,0],
    [0,6,6,0],
    [0,0,0,0]
  ],
  // mino_angle_90
  [
    [0,6,0,0],
    [0,6,6,6],
    [0,0,0,0],
    [0,0,0,0]
  ],
  // mino_angle_180
  [
    [0,6,6,0],
    [0,6,0,0],
    [0,6,0,0],
    [0,0,0,0]
  ],
  // mino_angle_270
  [
    [6,6,6,0],
    [0,0,6,0],
    [0,0,0,0],
    [0,0,0,0]
  ]],
// mino_type_L
  // mino_angle_0
  [[
    [0,7,0,0],
    [0,7,0,0],
    [0,7,7,0],
    [0,0,0,0]
  ],
  // mino_angle_90
  [
    [0,7,7,7],
    [0,7,0,0],
    [0,0,0,0],
    [0,0,0,0]
  ],
  // mino_angle_180
  [
    [0,7,7,0],
    [0,0,7,0],
    [0,0,7,0],
    [0,0,0,0]
  ],
  // mino_angle_270
  [
    [0,0,7,0],
    [7,7,7,0],
    [0,0,0,0],
    [0,0,0,0]
  ]],
// mino_type_T
  // mino_angle_0
  [[
    [8,8,8,0],
    [0,8,0,0],
    [0,0,0,0],
    [0,0,0,0]
  ],
  // mino_angle_90
  [
    [0,8,0,0],
    [8,8,0,0],
    [0,8,0,0],
    [0,0,0,0]
  ],
  // mino_angle_180
  [
    [0,8,0,0],
    [8,8,8,0],
    [0,0,0,0],
    [0,0,0,0]
  ],
  // mino_angle_270
  [
    [0,8,0,0],
    [0,8,8,0],
    [0,8,0,0],
    [0,0,0,0]
  ]]
]



// ============ 関数 ============

// フィールドの表示
function display() {
  for (let i = 0; i < FIELD_Y; i++) {
    for (let j =0; j < FIELD_X; j++) {
      // 塗りつぶしの四角を描画
      conText.fillStyle = COLOR[field[i][j]]
      conText.fillRect(MINO_SIZE * j, MINO_SIZE * i, MINO_SIZE, MINO_SIZE)

      // 輪郭の四角を描画
      conText.strokeStyle = COLOR[1]
      conText.lineWidth = 1
      conText.strokeRect(MINO_SIZE * j, MINO_SIZE * i, MINO_SIZE, MINO_SIZE)
    }
  }
}

// テトリミノの表示
function drawmino() {
  for (let i = 0; i < 4; i++) {
    for (let j = 0; j < 4; j++) {
      if (minoshapes[mino_type][mino_angle][i][j] > 1) {
        // 新しい座標を定義
        let px = MINO_SIZE * (mino_x + j)
        let py = MINO_SIZE * (mino_y + i)

        // 塗りつぶしの四角を描画
        conText.fillStyle = COLOR[minoshapes[mino_type][mino_angle][i][j]]
        conText.fillRect(px, py, MINO_SIZE, MINO_SIZE)

        // 輪郭の四角を描画
        conText.strokeStyle = COLOR[1]
        conText.lineWidth = 1
        conText.strokeRect(px, py, MINO_SIZE, MINO_SIZE)
      }
    }
  }
}

// テトリミノの落下
function dropmino() {
  if (inhit(mino_type,mino_angle,mino_x,mino_y + 1)) {
    // 動けるなら落下
    mino_y += 1
    display()
    drawmino()

  } else {
    // 落下したテトリミノをフィールド情報に記憶
    for (let i = 0; i < 4; i++) {
      for (let j = 0; j < 4; j++) {
        if (mino_y + i < FIELD_Y && field[mino_y + i][mino_x + j] == 0 && minoshapes[mino_type][mino_angle][i][j] >= 1) {
          field[mino_y + i][mino_x + j] = minoshapes[mino_type][mino_angle][i][j]
        }
      }
    }

    // ラインが揃ったらブロックを消す処理
    for (let i = 0; i < FIELD_Y; i++) {
      let linefull = true
      for (let j = 0; j < FIELD_X; j++) {
        if (field[i][j] == 0) {
          linefull = false
          break
        }
      }
      if (linefull) {
        for (let j = i; j >= 0; j--) {
          field[j] = field[j - 1]
        }
        field_a = []
        for (let k = 0; k < FIELD_Y; k++) {
          field_a.push(0)
        }
        field[0] = field_a

      } else {
        // ゲームオーバー
        if (mino_y == 0) {
          // ゲームオーバーの処理
          alert("ゲームオーバー")
          clearInterval(interval)
          return

        }
      }
    }

    // 次に落下するテトリミノを作成
    mino_x = Math.ceil((FIELD_X / 2) - 2)
    mino_y = 0
    mino_type = Math.trunc(Math.random() * 7)
    mino_angle = 0

    // 次に落下するテトリミノの表示
    display()
    drawmino()

  }
}


// テトリミノの当たり判定
function inhit(minotype,minoangle,minox,minoy) {
  for (let i = 0; i < 4; i++) {
    for (let j = 0; j < 4; j++) {
      if (minoshapes[minotype][minoangle][i][j] > 1) {
        let x = minox + j
        let y = minoy + i
        if (x < 0 || x >=  FIELD_X || y >= FIELD_Y || field[y][x] > 0) {
          return false
        }
      }
    }
  }
  return true
}



// ========= スマホ操作 ==========

// スワイプ開始時の座標
let startX
let startY

// スワイプ終了時の座標
let endX
let endY

canvas.addEventListener("touchstart", SwipeStart)
canvas.addEventListener("touchmove", SwipeMove)
canvas.addEventListener("touchend", SwipeEnd)

function SwipeStart(event) {
  // スワイプ開始時の座標
  startX = event.touches[0].pageX
  startY = event.touches[0].pageY

  // スワイプ終了時の座標
  endX = 0
  endY = 0
}

function SwipeMove(event) {
  // 画面をスクロールしないようにする(画面固定)
  event.preventDefault()

  endX = event.changedTouches[0].pageX
  endY = event.changedTouches[0].pageY

  if (endX - startX < -80) {
    startX -= 50
    if (inhit(mino_type,mino_angle,mino_x - 1,mino_y)) {
      // 動けるなら左移動
      mino_x -= 1
      display()
      drawmino()
    }
  }

  if (endX - startX > 80) {
    startX += 50
    if (inhit(mino_type,mino_angle,mino_x + 1,mino_y)) {
      // 動けるなら右移動
      mino_x += 1
      display()
      drawmino()
    }
  }

  if (endY - startY > 160) {
    if (inhit(mino_type,mino_angle,mino_x,mino_y + 1)) {
      // 動けるなら下移動
      mino_y += 1
      display()
      drawmino()
    }
  }
}

function SwipeEnd(event) {
  if (endX == 0) {
    if (inhit(mino_type,(mino_angle + 1) % 4,mino_x,mino_y)) {
      // 動けるなら回転
      mino_angle = (mino_angle + 1) % 4
      display()
      drawmino()
    }
  }
}



// ============ 実行 ============

// 初期画面の表示
display()
drawmino()
interval = setInterval(dropmino, DROP_SPEED)

JavaScriptの解説

今回新しく追加したのは、「// テトリミノの落下」のところです。

// テトリミノの落下
function dropmino() {
  if (inhit(mino_type,mino_angle,mino_x,mino_y + 1)) {
    // 動けるなら落下
    mino_y += 1
    display()
    drawmino()

  } else {
    // 落下したテトリミノをフィールド情報に記憶
    for (let i = 0; i < 4; i++) {
      for (let j = 0; j < 4; j++) {
        if (mino_y + i < FIELD_Y && field[mino_y + i][mino_x + j] == 0 && minoshapes[mino_type][mino_angle][i][j] >= 1) {
          field[mino_y + i][mino_x + j] = minoshapes[mino_type][mino_angle][i][j]
        }
      }
    }

    // ラインが揃ったらブロックを消す処理
    for (let i = 0; i < FIELD_Y; i++) {
      let linefull = true
      for (let j = 0; j < FIELD_X; j++) {
        if (field[i][j] == 0) {
          linefull = false
          break
        }
      }
      if (linefull)  {
        for (let j = i; j >= 0; j--) {
          field[j] = field[j - 1]
          field_a = []
          for (let i = 0; i < FIELD_Y; i++) {
            field_a.push(0)
          }
          field[0] = field_a
        }

      } else {
        // ゲームオーバー
        if (mino_y == 0) {
          // ゲームオーバーの処理
          alert("ゲームオーバー")
          clearInterval(interval)
          return
        }

      }
    }

    // 次に落下するテトリミノを作成
    mino_x = parseInt((FIELD_X / 2) - 2, 10)
    mino_y = 0
    mino_type = Math.floor( Math.random() * 7 )
    mino_angle = 0

    // 次に落下するテトリミノの表示
    display()
    drawmino()

  }
}

テトリミノが一番下まで落下したら、次に落下するテトリミノが現れるようにしなければいけません。

その前に、落下したテトリミノをフィールド情報に記憶させて、ブロックが横一列に揃っていたらラインを消す処理を行います。

具体的な流れは、

テトリミノが一番下まで落下したら
1.落下したテトリミノをフィールド情報に記憶する
2.ブロックが横一列に揃ったらラインを消す
3.テトリミノが一番上までいったらゲームオーバー
4.次に落下するテトリミノを作成する

となります。

function dropmino() { .. }

テトリミノが1つ下のマスに移動する関数です。dropmino() で関数を呼び出すことができます。

if .. else
if (inhit(mino_type,mino_angle,mino_x,mino_y + 1)) {
  :
  :
} else {
  :
  :
}

if文は、条件を満たすときに { } 内の処理を行います。条件を満たさないときは { } 内の処理を飛ばします。

else があれば、if文の条件を満たさないときに else の { } 内の処理を行います。

つまり、テトリミノが1マス下に動けるなら if の { } 内の処理を行い、1マス下に動けない(テトリミノが一番下まで落下した)なら else の { } 内の処理を行います。

次からは、テトリミノが一番下まで落下したときの else の { } 内の処理を順番に見ていきます。

落下したテトリミノをフィールド情報に記憶する

// 落下したテトリミノをフィールド情報に記憶
for (let i = 0; i < 4; i++) {
  for (let j = 0; j < 4; j++) {
    if (mino_y + i < FIELD_Y && field[mino_y + i][mino_x + j] == 0 && minoshapes[mino_type][mino_angle][i][j] >= 1) {
      field[mino_y + i][mino_x + j] = minoshapes[mino_type][mino_angle][i][j]
    }
  }
}

for文と if文を使って、2次元配列の変数field の値を変更してフィールド情報に記憶します。

for (let i = 0; i < 4; i++) { |for (let j = 0; j < 4; j++) { .. } | }

for文です。テトリミノの縦4マスと横4マスの値を1つずつ確認します。

if (mino_y + i < FIELD_Y && field[mino_y + i][mino_x + j] == 0 && minoshapes[mino_type][mino_angle][i][j] >= 1) { .. }

if文です。条件にある「 && 」は、「and」「かつ」という意味を持ちます。

if (条件1 && 条件2 && 条件3) { .. } の場合、すべての条件を満たすときに { } 内の処理を行います。

テトリミノのブロックがフィールド内にあり、フィールド情報が「 0 」で、テトリミノ情報が「 1 」以上のすべての条件を満たせば { } 内の処理を行います。

mino_y + i < FIELD_Y

変数mino_y は、テトリミノの左上の縦座標です。変数 i は、テトリミノの縦何マス目かです。

変数FIELD_Y は、フィールドの縦のマス数です。

つまり、「テトリミノのある1ブロックがフィールド内にあるとき」です。

field[mino_y + i][mino_x + j] == 0

mino_y + i は、テトリミノの1ブロックの縦座標です。

mino_x + j は、テトリミノの1ブロックの横座標です。

つまり、「テトリミノのある1ブロックがあるフィールド内の座標が 0 のとき」です。

minoshapes[mino_type][mino_angle][i][j] >= 1

mino_type は、テトリミノの形(種類)です。

mino_angle は、テトリミノの回転角度です。

多次元配列の変数minoshapes は、[(テトリミノの形), (回転角度), (縦), (横)] で1つ値を呼び出すことができます。

つまり、「テトリミノ情報にブロックが存在する(値が 1 以上)のとき」です。

field[mino_y + i][mino_x + j] = minoshapes[mino_type][mino_angle][i][j]

if文内の処理です。2次元配列の変数field の値を変更してフィールド情報に記憶します。

変数field のテトリミノのブロックが存在する座標の値を、テトリミノの種類の値(2〜8)に書き換えます。

ブロックが横一列に揃ったらラインを消す

// ラインが揃ったらブロックを消す処理
for (let i = 0; i < FIELD_Y; i++) {
  let linefull = true
  for (let j = 0; j < FIELD_X; j++) {
    if (field[i][j] == 0) {
      linefull = false
      break
    }
  }
  if (linefull) {
    for (let j = i; j >= 0; j--) {
      field[j] = field[j - 1]
    }
    field_a = []
    for (let k = 0; k < FIELD_Y; k++) {
      field_a.push(0)
    }
    field[0] = field_a

  } else {
  :
  :
  }
}

フィールドの横一列の値を調べて、すべてにブロックがあれば(「 0 」が1つもなければ )ラインを消します。

横一列すべてにブロックがあるのを調べるには、『「ブロックがないマスが存在する」ことがない』という二重否定を利用するのが簡単です。

二重否定は、否定を2つ重ねて肯定の意味を持ちます。

for (let i = 0; i < FIELD_Y; i++) { .. }

for文です。フィールドの上から横一列ずつ調べます。

let linefull = true

変数linefull の値を「true」に指定します。

ブロックがないマスが存在するとき、変数linefull の値を「false」に変更します。

for (let j = 0; j < FIELD_X; j++) { .. }

for文です。フィールドのある一列を1マスずつ調べます。

if (field[i][j] == 0) { .. }

if文です。2次元配列の変数field にある値が 0 のとき、つまりブロックがないマスのときに { } 内の処理を行います。

linefull = false

変数linefull の値を「false」に変更します。

break

break で、for文を途中で終了して抜けます。

変数linefull は、横一列すべてにブロックがあれば「true」、ブロックがないマスが存在すれば「false」になります。

if (linefull) { .. }

if文です。変数linefull が「true」のとき、つまり横一列すべてにブロックがあるときに { } 内の処理を行います。

for (let j = i; j >= 0; j--) {
  field[j] = field[j - 1]
}

変数field[j] は、上から j 行目の横一列のブロックの情報です。

i 行目の横一列すべてにブロックがあるので、i 行目( j 行目)のブロックを1つ上の横一列 ( j – 1 )行目に置き換えます。

それを繰り返して、一番上の1行目のブロックが2行目に置き換わります。このままでは、一番上の1行目のブロックは残ったままです。

一番上の1行目のブロックを消します。

field_a = []
for (let k = 0; k < FIELD_X; k++) {
  field_a.push(0)
}
field[0] = field_a

この部分は、field[0] = [0,0,0,0,0,0,0,0,0,0] と同じです。

フィールドの横のマス数を自由に変えれるように、このようにしています。

配列の変数field_a を作り、フィールドの横のマス数だけ 0 を追加しています。

それを一番上の1行目のブロック(field[0])へ置き換えます。

フィールドの横のマス数を変えることがなければ、

field[0] = [0,0,0,0,0,0,0,0,0,0]

だけでもオッケーです。

テトリミノが一番上までいったらゲームオーバー

// ラインが揃ったらブロックを消す処理
for (let i = 0; i < FIELD_Y; i++) {
  :
  :
  if (linefull)  {
  :
  :
  } else {
    // ゲームオーバー
    if (mino_y == 0) {
      // ゲームオーバーの処理
      alert("ゲームオーバー")
      clearInterval(interval)
      return
    }

  }
}

テトリミノが一番下まで落下してラインが揃わなかったとき、テトリミノが一番上までいったらゲームオーバーの処理を行います。

「ゲームオーバー」の文字を表示して、繰り返し処理を終了します。

if (mino_y == 0) { .. }

if文です。変数mino_y が 0 のときに { } 内の処理を行います。

変数mino_y は、テトリミノの y座標です。「 0 」のときは、落下したテトリミノが一番上になったときです。

alert(“ゲームオーバー”)

alert(…) は、メッセージをダイアログとして表示します。

本来は警告用ダイアログなのですが、メッセージを手軽に表示できるため警告以外の目的で使用されることも多いです。

clearInterval(interval)

clearInterval() は setInterval() で設定した繰り返し処理を解除します。

interval = setInterval(dropmino, DROP_SPEED) で、一定時間が経過するたびに関数dropmino を実行しています。

return

return で、関数の実行を終了します。関数(function)の { } 内にあるそれ以降の処理を行いません。

次に落下するテトリミノを作成する

// 次に落下するテトリミノを作成
mino_x = Math.ceil((FIELD_X / 2) - 2)
mino_y = 0
mino_type = Math.trunc(Math.random() * 7)
mino_angle = 0

// 次に落下するテトリミノの表示
display()
drawmino()

テトリミノが一番下まで落下したら、落下したテトリミノをフィールド情報に記憶して、ラインが揃ったらブロックを消して、ゲームオーバーにならなかったら、次に落下するテトリミノを作成して表示します。

テトリミノの初期設定をしたときと同じ処理です。

mino_x = Math.ceil((FIELD_X / 2) – 2)

テトリミノを描画する x座標「mino_x」を指定します。座標は、テトリミノ 4×4マスの左上の座標です。

FIELD_X(フィールドの横のマス数)を 2で割って、2マス引きます。FIELD_X が奇数のとき、2で割ると端数が出るので Math.ceil() を使って小数部分を切り上げます。

mino_y = 0

テトリミノを描画する y座標「mino_y」を指定します。座標は、テトリミノ 4×4 マスの左上の座標です。

「 0 」は、一番上です。

mino_type = Math.trunc(Math.random() * 7)

テトリミノの形を7種類の中から1つ選びます。

Math.random() は、0 から 1 未満の小数をランダムで生成します。

生成された小数を7倍して小数部分を切り捨てると「0, 1, 2, 3, 4, 5, 6」の7つの中から1つの値が取得できます。

Math.trunc( .. ) は、小数部分を切り捨てた値を取得します。

mino_angle = 0

テトリミノの回転した形を指定します。「 0 」は、回転していない状態です。

display()

フィールドを表示する関数を呼び出します。

drawmino()

テトリミノを表示する関数を呼び出します。

新しいフィールドを描画して、新しく作成したテトリミノを描画します。

テトリス完成

これでテトリスが遊べるかたちになりました。

フィールドサイズ(横のマスの数)の値を変えたり、テトリミノが落ちる速度の値を変えたりして遊んでもおもしろいと思います。

これで終わりにせずに、まだまだ作り込むことも可能です。

レイアウトを変更したり、次に落ちるテトリミノを表示したり、消したライン数やスコアを追加してもいいです。

消したライン数やスコアによって、テトリミノの落下スピードを早くしてもいいです。

「好きなようにカスタマイズして自分で組んだプログラムを実行して動かして、プログラミングを楽しんでみてください。」

最後にJavaScript全体のソースコードを載せておきます。

// ============ 変数 ============

// フィールドサイズ
const FIELD_X = 10   // 横のマスの数
const FIELD_Y = 20   // 縦のマスの数
const MINO_SIZE = 30 // マスの大きさ

// テトリミノが落ちる速度
const DROP_SPEED = 500;

// キャンバスの設定
let canvas = document.getElementById("canvas01")
let conText = canvas.getContext("2d")
canvas.width = MINO_SIZE * FIELD_X
canvas.height = MINO_SIZE * FIELD_Y
canvas.style.border = "4px solid #050505"

// 色の設定
const COLOR = [
  "#F2F2F2",   // フィールド背景
  "#C8C8C8",   // 枠の線
  "#00F2F2",   // Iミノ 水色
  "#F2F200",   // Oミノ 黄色
  "#00F200",   // Sミノ 黄緑
  "#F20000",   // Zミノ 赤
  "#0000F2",   // Jミノ 青
  "#F2A200",   // Lミノ オレンジ
  "#F200F2"    // Tミノ 紫
]

// フィールドの初期設定
let field = []
for (let i = 0; i < FIELD_Y; i++) {
  field[i] = []
  for (let j = 0; j < FIELD_X; j++) {
    field[i][j] = 0
  }
}

// テトリミノの初期設定
let mino_x = Math.ceil((FIELD_X / 2) - 2)
let mino_y = 0

let mino_type = Math.trunc(Math.random() * 7)
let mino_angle = 0

let minoshapes = [
// mino_type_I
  // mino_angle_0
  [[
    [0,2,0,0],
    [0,2,0,0],
    [0,2,0,0],
    [0,2,0,0]
  ],
  // mino_angle_90
  [
    [0,0,0,0],
    [0,0,0,0],
    [2,2,2,2],
    [0,0,0,0]
  ],
  // mino_angle_180
  [
    [0,2,0,0],
    [0,2,0,0],
    [0,2,0,0],
    [0,2,0,0]
  ],
  // mino_angle_270
  [
    [0,0,0,0],
    [0,0,0,0],
    [2,2,2,2],
    [0,0,0,0]
  ]],
// mino_type_O
  // mino_angle_0
  [[
    [0,3,3,0],
    [0,3,3,0],
    [0,0,0,0],
    [0,0,0,0]
  ],
  // mino_angle_90
  [
    [0,3,3,0],
    [0,3,3,0],
    [0,0,0,0],
    [0,0,0,0]
  ],
  // mino_angle_180
  [
    [0,3,3,0],
    [0,3,3,0],
    [0,0,0,0],
    [0,0,0,0]
  ],
  // mino_angle_270
  [
    [0,3,3,0],
    [0,3,3,0],
    [0,0,0,0],
    [0,0,0,0]
  ]],
// mino_type_S
  // mino_angle_0
  [[
    [0,4,4,0],
    [4,4,0,0],
    [0,0,0,0],
    [0,0,0,0]
  ],
  // mino_angle_90
  [
    [0,4,0,0],
    [0,4,4,0],
    [0,0,4,0],
    [0,0,0,0]
  ],
  // mino_angle_180
  [
    [0,4,4,0],
    [4,4,0,0],
    [0,0,0,0],
    [0,0,0,0]
  ],
  // mino_angle_270
  [
    [0,4,0,0],
    [0,4,4,0],
    [0,0,4,0],
    [0,0,0,0]
  ]],
// mino_type_Z
  // mino_angle_0
  [[
    [5,5,0,0],
    [0,5,5,0],
    [0,0,0,0],
    [0,0,0,0]
  ],
  // mino_angle_90
  [
    [0,0,5,0],
    [0,5,5,0],
    [0,5,0,0],
    [0,0,0,0]
  ],
  // mino_angle_180
  [
    [5,5,0,0],
    [0,5,5,0],
    [0,0,0,0],
    [0,0,0,0]
  ],
  // mino_angle_270
  [
    [0,0,5,0],
    [0,5,5,0],
    [0,5,0,0],
    [0,0,0,0]
  ]],
// mino_type_J
  // mino_angle_0
  [[
    [0,0,6,0],
    [0,0,6,0],
    [0,6,6,0],
    [0,0,0,0]
  ],
  // mino_angle_90
  [
    [0,6,0,0],
    [0,6,6,6],
    [0,0,0,0],
    [0,0,0,0]
  ],
  // mino_angle_180
  [
    [0,6,6,0],
    [0,6,0,0],
    [0,6,0,0],
    [0,0,0,0]
  ],
  // mino_angle_270
  [
    [6,6,6,0],
    [0,0,6,0],
    [0,0,0,0],
    [0,0,0,0]
  ]],
// mino_type_L
  // mino_angle_0
  [[
    [0,7,0,0],
    [0,7,0,0],
    [0,7,7,0],
    [0,0,0,0]
  ],
  // mino_angle_90
  [
    [0,7,7,7],
    [0,7,0,0],
    [0,0,0,0],
    [0,0,0,0]
  ],
  // mino_angle_180
  [
    [0,7,7,0],
    [0,0,7,0],
    [0,0,7,0],
    [0,0,0,0]
  ],
  // mino_angle_270
  [
    [0,0,7,0],
    [7,7,7,0],
    [0,0,0,0],
    [0,0,0,0]
  ]],
// mino_type_T
  // mino_angle_0
  [[
    [8,8,8,0],
    [0,8,0,0],
    [0,0,0,0],
    [0,0,0,0]
  ],
  // mino_angle_90
  [
    [0,8,0,0],
    [8,8,0,0],
    [0,8,0,0],
    [0,0,0,0]
  ],
  // mino_angle_180
  [
    [0,8,0,0],
    [8,8,8,0],
    [0,0,0,0],
    [0,0,0,0]
  ],
  // mino_angle_270
  [
    [0,8,0,0],
    [0,8,8,0],
    [0,8,0,0],
    [0,0,0,0]
  ]]
]



// ============ 関数 ============

// フィールドの表示
function display() {
  for (let i = 0; i < FIELD_Y; i++) {
    for (let j =0; j < FIELD_X; j++) {
      // 塗りつぶしの四角を描画
      conText.fillStyle = COLOR[field[i][j]]
      conText.fillRect(MINO_SIZE * j, MINO_SIZE * i, MINO_SIZE, MINO_SIZE)

      // 輪郭の四角を描画
      conText.strokeStyle = COLOR[1]
      conText.lineWidth = 1
      conText.strokeRect(MINO_SIZE * j, MINO_SIZE * i, MINO_SIZE, MINO_SIZE)
    }
  }
}

// テトリミノの表示
function drawmino() {
  for (let i = 0; i < 4; i++) {
    for (let j = 0; j < 4; j++) {
      if (minoshapes[mino_type][mino_angle][i][j] > 1) {
        // 新しい座標を定義
        let px = MINO_SIZE * (mino_x + j)
        let py = MINO_SIZE * (mino_y + i)

        // 塗りつぶしの四角を描画
        conText.fillStyle = COLOR[minoshapes[mino_type][mino_angle][i][j]]
        conText.fillRect(px, py, MINO_SIZE, MINO_SIZE)

        // 輪郭の四角を描画
        conText.strokeStyle = COLOR[1]
        conText.lineWidth = 1
        conText.strokeRect(px, py, MINO_SIZE, MINO_SIZE)
      }
    }
  }
}

// テトリミノの落下
function dropmino() {
  if (inhit(mino_type,mino_angle,mino_x,mino_y + 1)) {
    // 動けるなら落下
    mino_y += 1
    display()
    drawmino()

  } else {
    // 落下したテトリミノをフィールド情報に記憶
    for (let i = 0; i < 4; i++) {
      for (let j = 0; j < 4; j++) {
        if (mino_y + i < FIELD_Y && field[mino_y + i][mino_x + j] == 0 && minoshapes[mino_type][mino_angle][i][j] >= 1) {
          field[mino_y + i][mino_x + j] = minoshapes[mino_type][mino_angle][i][j]
        }
      }
    }

    // ラインが揃ったらブロックを消す処理
    for (let i = 0; i < FIELD_Y; i++) {
      let linefull = true
      for (let j = 0; j < FIELD_X; j++) {
        if (field[i][j] == 0) {
          linefull = false
          break
        }
      }
      if (linefull) {
        for (let j = i; j >= 0; j--) {
          field[j] = field[j - 1]
        }
        field_a = []
        for (let k = 0; k < FIELD_Y; k++) {
          field_a.push(0)
        }
        field[0] = field_a

      } else {
        // ゲームオーバー
        if (mino_y == 0) {
          // ゲームオーバーの処理
          alert("ゲームオーバー")
          clearInterval(interval)
          return

        }
      }
    }

    // 次に落下するテトリミノを作成
    mino_x = Math.ceil((FIELD_X / 2) - 2)
    mino_y = 0
    mino_type = Math.trunc(Math.random() * 7)
    mino_angle = 0

    // 次に落下するテトリミノの表示
    display()
    drawmino()

  }
}


// テトリミノの当たり判定
function inhit(minotype,minoangle,minox,minoy) {
  for (let i = 0; i < 4; i++) {
    for (let j = 0; j < 4; j++) {
      if (minoshapes[minotype][minoangle][i][j] > 1) {
        let x = minox + j
        let y = minoy + i
        if (x < 0 || x >=  FIELD_X || y >= FIELD_Y || field[y][x] > 0) {
          return false
        }
      }
    }
  }
  return true
}



// ========= スマホ操作 ==========

// スワイプ開始時の座標
let startX
let startY

// スワイプ終了時の座標
let endX
let endY

canvas.addEventListener("touchstart", SwipeStart)
canvas.addEventListener("touchmove", SwipeMove)
canvas.addEventListener("touchend", SwipeEnd)

function SwipeStart(event) {
  // スワイプ開始時の座標
  startX = event.touches[0].pageX
  startY = event.touches[0].pageY

  // スワイプ終了時の座標
  endX = 0
  endY = 0
}

function SwipeMove(event) {
  // 画面をスクロールしないようにする(画面固定)
  event.preventDefault()

  endX = event.changedTouches[0].pageX
  endY = event.changedTouches[0].pageY

  if (endX - startX < -80) {
    startX -= 50
    if (inhit(mino_type,mino_angle,mino_x - 1,mino_y)) {
      // 動けるなら左移動
      mino_x -= 1
      display()
      drawmino()
    }
  }

  if (endX - startX > 80) {
    startX += 50
    if (inhit(mino_type,mino_angle,mino_x + 1,mino_y)) {
      // 動けるなら右移動
      mino_x += 1
      display()
      drawmino()
    }
  }

  if (endY - startY > 160) {
    if (inhit(mino_type,mino_angle,mino_x,mino_y + 1)) {
      // 動けるなら下移動
      mino_y += 1
      display()
      drawmino()
    }
  }
}

function SwipeEnd(event) {
  if (endX == 0) {
    if (inhit(mino_type,(mino_angle + 1) % 4,mino_x,mino_y)) {
      // 動けるなら回転
      mino_angle = (mino_angle + 1) % 4
      display()
      drawmino()
    }
  }
}



// ============ 実行 ============

// 初期画面の表示
display()
drawmino()
interval = setInterval(dropmino, DROP_SPEED)