JavaScript プログラミング

JavaScriptでテトリスを作ってみた。

2023年9月3日

  1. HOME >
  2. プログラミング >
  3. JavaScript >

JavaScriptでテトリスを作ってみた。

2023年9月3日

こんにちはyukiです。

今回は、JavaScriptでテトリスを作りましたので、記録しておきます。
(ネットの記事を結構参考にしました。一部動かなかったのでそこはなんとか頑張りました。)

今後、C#などでも作りたい。。。

テトリスのリンクはこちら

※PC用です。

ソースコード

ソースコードも置いておきます。

HTML

<!DOCTYPE html>
<html lang="jan">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link
      rel="stylesheet"
      href="/tetris.css"
    />
    <title>テトリス</title>
  </head>
  <body>
    <div id="container">
      <canvas id="main-canvas"></canvas>
      <canvas id="next-canvas"></canvas>
      <img
        id="img-howto"
        src="/tetris/images/howto.png"
        alt=""
      />
      <button id="start-btn">開始</button>
    </div>
    <script src="/tetris/tetris.js"></script>
  </body>
</html>

CSS

#container {
  position: relative;
  margin: auto;
  width: 700px;
  height: 600px;
}

#main-canvas {
  position: absolute;
  top: 0;
  left: 0;
  width: 360px;
  height: 600px;
}

#next-canvas {
  position: absolute;
  top: 0;
  left: 400px;
}

#img-howto {
  position: absolute;
  bottom: 200px;
  left: 380px;
}

#start-btn {
  position: absolute;
  left: 400px;
  bottom: 0;
  width: 180px;
  text-align: center;
  font-size: 16px;
  color: #fff;
  text-decoration: none;
  font-weight: bold;
  padding: 12px 24px;
  border: none;
  border-radius: 4px;
  background-image: linear-gradient(to top, #c471f5 0%, #fa71cd 100%);
  transition: 0.5s;
  background-size: 200%;
}

#start-btn:hover {
  background-position: right center;
}

JavaScript

const START_BTN_ID = "start-btn";
const MAIN_CANVAS_ID = "main-canvas";
const NEXT_CANVAS_ID = "next-canvas";
const GAME_SPEED = 500;
const BLOCK_SIZE = 32;
const COLS_COUNT = 10;
const ROWS_COUNT = 20;
const SCREEN_WIDTH = COLS_COUNT * BLOCK_SIZE;
const SCREEN_HEIGHT = ROWS_COUNT * BLOCK_SIZE;
const NEXT_AREA_SIZE = 160;
const BLOCK_SOURCE = [
  "images/block-0.png",
  "images/block-1.png",
  "images/block-2.png",
  "images/block-3.png",
  "images/block-4.png",
  "images/block-5.png",
  "images/block-6.png",
];

window.onload = function () {
  Asset.init(() => {
    // initメソッドのコールバックを追加
    let game = new Game(); // GamepadではなくGameクラスを生成
    document.getElementById(START_BTN_ID).onclick = function () {
      game.start();
      this.blur();
    };
  });
};

// 素材を管理するクラス
// ゲーム開始前に初期化する
class Asset {
  // ブロック用Imageの配列
  static blockImage = [];

  // 初期化処理
  // callback には、init完了後に行う処理を渡す
  static init(callback) {
    let loadCnt = 0;
    for (let i = 0; i <= 7; i++) {
      // i <= 7 に修正
      let img = new Image();
      img.src = BLOCK_SOURCE[i];
      img.onload = function () {
        loadCnt++;
        Asset.blockImage.push(img);

        // 全ての画像読み込みが終われば、callback実行
        if (loadCnt >= BLOCK_SOURCE.length && callback) {
          callback();
        }
      };
    }
  }
}

class Game {
  constructor() {
    this.initMainCanvas();
    this.initNextCanvas();
  }

  // メインキャンバスの初期化
  initMainCanvas() {
    this.mainCanvas = document.getElementById(MAIN_CANVAS_ID);
    this.mainCtx = this.mainCanvas.getContext("2d");
    this.mainCanvas.width = SCREEN_WIDTH;
    this.mainCanvas.height = SCREEN_HEIGHT;
    this.mainCanvas.style.border = "4px solid #555";
  }

  // ネクストキャンバスの初期化
  initNextCanvas() {
    this.nextCanvas = document.getElementById(NEXT_CANVAS_ID);
    this.nextCtx = this.nextCanvas.getContext("2d");
    this.nextCanvas.width = NEXT_AREA_SIZE;
    this.nextCanvas.height = NEXT_AREA_SIZE;
    this.nextCanvas.style.border = "4px solid #555";
  }

  // ゲーム開始の処理(STARTボタンクリック時)
  start() {
    // フィールドとミノを初期化
    this.field = new Field();

    // 最初のミノを読み込み
    this.popMino();

    // 初回描画
    this.drawAll();

    // 落下処理
    clearInterval(this.timer);
    this.timer = setInterval(() => this.dropMino(), 1000);

    // キーボードのイベント登録
    this.setKeyEvent();
  }

  // 新しいミノを読み込む
  popMino() {
    this.mino = this.nextMino || new Mino(); // もともとのコードに誤りがあったため修正
    this.mino.spawn();
    this.nextMino = new Mino();

    // ゲームオーバー判定
    if (!this.valid(0, 1)) {
      this.drawAll();
      clearInterval(this.timer); // clearIntervalのタイプミスを修正
      alert("ゲームオーバー");
    }
  }

  // 図形の描画
  drawAll() {
    // 表示クリア
    this.mainCtx.clearRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
    this.nextCtx.clearRect(0, 0, NEXT_AREA_SIZE, NEXT_AREA_SIZE);

    // 落下済みのミノを描画
    this.field.drawFixedBlocks(this.mainCtx);

    // 再描画
    this.nextMino.drawNext(this.nextCtx);
    this.mino.draw(this.mainCtx);
  }

  // ミノの落下処理
  dropMino() {
    if (this.valid(0, 1)) {
      this.mino.y++;
    } else {
      // Minoを固定する(座標変換してFieldに渡す)
      this.mino.blocks.forEach((e) => {
        e.x += this.mino.x;
        e.y += this.mino.y;
      });
      this.field.blocks = this.field.blocks.concat(this.mino.blocks);
      this.field.checkLine();
      this.popMino();
    }
    this.drawAll();
  }

  // 次の移動が可能かチェック
  valid(moveX, moveY, rot = 0) {
    let newBlocks = this.mino.getNewBlocks(moveX, moveY, rot);
    return newBlocks.every((block) => {
      return (
        block.x >= 0 &&
        block.y >= -1 &&
        block.x < COLS_COUNT &&
        block.y < ROWS_COUNT &&
        !this.field.has(block.x, block.y)
      );
    });
  }

  // キーボードイベント
  setKeyEvent() {
    window.addEventListener("keydown", (e) => {
      switch (e.keyCode) {
        case 37: // 左
          if (this.valid(-1, 0)) this.mino.x--;
          break;
        case 39: // 右
          if (this.valid(1, 0)) this.mino.x++;
          break;
        case 40: // 下
          if (this.valid(0, 1)) this.mino.y++;
          break;
        case 32: // スペースキー
          // スペースキーが押されたときに回転させる
          this.rotateMino();
          break;
      }
      this.drawAll();
    });
  }

  rotateMino() {
    if (this.valid(0, 0, 1)) {
      // 回転可能かチェック
      this.mino.rotate(); // ミノを回転させる
      this.drawAll(); // 画面を再描画
    }
  }
}

class Block {
  // 基準地点からの座標
  // 移動中 ⇒ Minoの左上
  // 配置後 ⇒ Fieldの左上
  constructor(x, y, type) {
    this.x = x;
    this.y = y;

    // 描画しないときはタイプを固定しない
    if (type >= 0) this.setType(type);
  }

  setType(type) {
    this.type = type;
    this.image = Asset.blockImage[type];
  }

  // Minoに属するときは、Minoの位置をオフセットに指定
  // Fieldに属するときは、(0, 0)を起点とするので不要
  draw(offsetX = 0, offsetY = 0, ctx) {
    let drawX = this.x + offsetX;
    let drawY = this.y + offsetY;

    // 画面外は描画しない
    if (drawX >= 0 && drawX < COLS_COUNT && drawY >= 0 && drawY < ROWS_COUNT) {
      ctx.drawImage(
        this.image,
        drawX * BLOCK_SIZE,
        drawY * BLOCK_SIZE,
        BLOCK_SIZE,
        BLOCK_SIZE
      );
    }
  }

  // 次のミノを描画する
  // タイプごとに余白を調整して、中央に表示
  drawNext(ctx) {
    let offsetX = 0;
    let offsetY = 0;
    switch (this.type) {
      case 0:
        offsetX = 0.5;
        offsetY = 0;
        break;
      case 1:
        offsetX = 0.5;
        offsetY = 0;
        break;
      default:
        offsetX = 1;
        offsetY = 0.5;
        break;
    }

    ctx.drawImage(
      this.image,
      (this.x + offsetX) * BLOCK_SIZE,
      (this.y + offsetY) * BLOCK_SIZE,
      BLOCK_SIZE,
      BLOCK_SIZE
    );
  }
}

class Mino {
  constructor() {
    this.type = Math.floor(Math.random() * 7); // Math.ramdom() ではなく Math.random()
    this.initBlocks();
  }

  initBlocks() {
    let t = this.type;
    switch (t) {
      case 0: // I型
        this.blocks = [
          new Block(0, 2, t),
          new Block(1, 2, t),
          new Block(2, 2, t),
          new Block(3, 2, t),
        ];
        break;
      case 1: // O型
        this.blocks = [
          new Block(1, 1, t),
          new Block(2, 1, t),
          new Block(1, 2, t),
          new Block(2, 2, t),
        ];
        break;
      case 2: // T型
        this.blocks = [
          new Block(1, 1, t),
          new Block(0, 2, t),
          new Block(1, 2, t),
          new Block(2, 2, t),
        ];
        break;
      case 3: // J型
        this.blocks = [
          new Block(1, 1, t),
          new Block(0, 2, t),
          new Block(1, 2, t),
          new Block(2, 2, t),
        ];
        break;
      case 4: // L型
        this.blocks = [
          new Block(2, 1, t),
          new Block(0, 2, t),
          new Block(1, 2, t),
          new Block(2, 2, t),
        ];
        break;
      case 5: // S型
        this.blocks = [
          new Block(1, 1, t),
          new Block(2, 1, t),
          new Block(0, 2, t),
          new Block(1, 2, t),
        ];
        break;
      case 6: // Z型
        this.blocks = [
          new Block(0, 1, t),
          new Block(1, 1, t),
          new Block(1, 2, t),
          new Block(2, 2, t),
        ];
        break;
    }
  }

  // フィールドに生成する
  spawn() {
    this.x = Math.floor(COLS_COUNT / 2) - 2; // Math.floor を追加
    this.y = -3;
  }

  // フィールドに描画する
  draw(ctx) {
    this.blocks.forEach((block) => {
      block.draw(this.x, this.y, ctx);
    });
  }

  // 次のミノを描画する
  drawNext(ctx) {
    this.blocks.forEach((block) => {
      block.drawNext(ctx);
    });
  }

  // ミノを回転させるメソッド
  rotate() {
    let centerX = 0;
    let centerY = 0;

    // ミノの中心座標を計算
    this.blocks.forEach((block) => {
      centerX += block.x;
      centerY += block.y;
    });
    centerX /= this.blocks.length;
    centerY /= this.blocks.length;

    let newBlocks = this.blocks.map((block) => {
      let relativeX = block.x - centerX;
      let relativeY = block.y - centerY;

      // 中心周りで回転
      let rotatedX = Math.round(centerX - relativeY);
      let rotatedY = Math.round(centerY + relativeX);

      return new Block(rotatedX, rotatedY, this.type);
    });

    this.blocks = newBlocks; // ブロックの位置を更新
  }

  getNewBlocks(moveX, moveY, rot) {
    let newBlocks = this.blocks.map((block) => {
      return new Block(block.x, block.y);
    });
    newBlocks.forEach((block) => {
      // 移動させる場合
      if (moveX || moveY) {
        block.x += moveX;
        block.y += moveY;
      }

      // 回転させる場合
      if (rot) {
        let oldX = block.x;
        block.x = block.y;
        block.y = 3 - oldX;
      }

      // グローバル座標に変換
      block.x += this.x;
      block.y += this.y;
    });

    return newBlocks;
  }
}

class Field {
  constructor() {
    this.blocks = [];
  }

  drawFixedBlocks(ctx) {
    this.blocks.forEach((block) => block.draw(0, 0, ctx));
  }

  checkLine() {
    for (var r = 0; r < ROWS_COUNT; r++) {
      var c = this.blocks.filter((block) => block.y === r).length;
      if (c === COLS_COUNT) {
        this.blocks = this.blocks.filter((block) => block.y !== r);
        this.blocks
          .filter((block) => block.y < r)
          .forEach((upper) => upper.y++);
      }
    }
  }

  has(x, y) {
    return this.blocks.some((block) => block.x == x && block.y == y);
  }
}
  • この記事を書いた人

元小売業のエンジニア

✅小売業から転職したITエンジニア
✅現在はシステム開発やweb開発、HP制作をしています
✅パソコンひとつでできる仕事について発信中

-JavaScript, プログラミング