[Vue3]テトリスを作ろう(実装編①)

# はじめに
 　Vue3の勉強としてある程度複雑なロジックを含むフロントエンドの実装をしてみたいと思いテトリスを作成しました。
  前回はふわっとした概要でしたが、今回はコードベースで実装を行っていきたいと思います。

アプリURL

https://naitoyuma7110.github.io/tetris/

## 前回の内容
- [x] テトリスの簡単な設計をまとめる
- [x] テトリスの主なロジックを図を使ってまとめる

https://qiita.com/naitoyuma/items/8dcbf6b64dbddffe0383

## 今回の内容
- [x] フィールドクラスを定義する
- [x] テトリミノクラスを定義する
- [x] フィールドとテトリミノを描画する

## テトリスの主ロジック

前回記事からの引用ですが、下記画像のフィールド、テトリミノクラスを定義していきます。

- フィールドとテトリミノの描画、テトリミノの移動、回転

![image](https://github.com/naitoyuma7110/tetris/assets/128150297/9307f8de-8180-4a1d-a12d-ded0231d0030)

## Fieldクラス作成
フィールドは縦20、横10マスのブロックを持ちます。
画面にフィールドを描画する際これを配列として持つのがシンプルだと思いますので、2次元配列として定義します。
フィールドの特定のブロックが空なのか、あるいはいずれかのタイプのテトリミノがあるかはField[Y(縦)][X(横)]で配列の値を取得する事で確認できます。
(座標(x,y)とアクセスの順番が逆でややこしいです)


```ts:src/common/field.ts
// Fieldクラス
export class field {
  private row = 20
  private col = 10

  renderField: number[][]

  constructor() {
    const field = new Array<Array<number>>(this.row)

    for (let i = 0; i < this.row; i++) {
      const fieldColumn = new Array(this.col).fill(0)
      field[i] = fieldColumn
    }

    this.renderField = field
  }
}
```

描画用の配列renderFieldを定義し、インスタンス化するときに全てのブロックを空(0とする)で埋めます。
縦20、横10のブロック数は定数で今のところクラス内でしか使わないのでprivateとしました。

これをVueで描画します。
```vuejs:src/pages/PlayPage.vue
<script setup lang="ts">
import { field } from "@/common/field";
import { reactive } from "vue";

const field = reactive(new Field());
</script>

<template>
  <div>{{ field.renderField }}</div>
</template>
```
オブジェクトに対してリアクティブ(値の変更を感知できる状態)を設定するためreactiveを使用しています。
Vueにおいてクラスインスタンスは状態管理できるのか疑問でしたが、クラス内でVue固有の機能を使う際には下記記事のような制約があるとの事で大変参考になりました。

 https://zenn.dev/tanukikyo/articles/40603fbdc88c05

今回テトリスではクラス自体は通常通りの定義をするので多分問題ないと思います。

vue-routerを導入して上記で作成したゲーム画面へのルーティングを定義します。

npmでインストール
```
npm i vue-router
```

vue-router使用準備
```ts:main.ts
import App from './App.vue'
import router from './router'

const app = createApp(App)
app.use(router).mount('#app')
```

ルーティング定義
```ts:src/router/index.ts
import { createRouter, createWebHistory } from 'vue-router'

import PlayPage from '@/pages/PlayPage.vue'

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: '/play',
      name: 'PlayPage',
      component: PlayPage
    }
  ]
})

export default router
```


ルート定義ができたらviteで開発サーバを立ち上げて画面を確認します。
```
npx vite --port=4000
```
ブラウザでhttp://localhost:4000/play へアクセス

0で初期化したフィールド配列がそのまんま表示されると思うのでPlayGame.vueのtemplate部分を修正し列はdiv、マス目はspanで描画します。
```vuejs:src/pages/PlayGame.vue
<template>
  <div v-for="(row, i) in field.renderField" :key="i">
    <span v-for="(col, j) in row" :key="j">
      {{ col }}
    </span>
  </div>
</template>
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/601137/16fb284c-bbf0-fe33-2c95-cbcc3a2ec4c2.png)

スタイルを調整します。

せっかくなのでcssはScssを使ってみます。
```
npm install sass
```

HTML要素にclss名を付けます。
```vue:src/pages/PlayGame.vue
<template>
  <div class="field">
    <span class="field-row" v-for="(row, i) in field.renderField" :key="i">
      <span :class="`field-col block-${col}`" v-for="(col, j) in row" :key="j">
        {{ col }}
      </span>
    </span>
  </div>
</template>
```
```scss
<style lang="scss">
.field {
  display: inline-block;
  border: 5px solid #ccc;
  border-top: none;

  &-row {
    display: flex;
  }

  &-col {
    width: 2rem;
    height: 2rem;
    text-align: center;
    border: 0.1px solid #eee;
  }
  .block {
    &-0 {
      background: #ccc;
    }
  }
}
</style>
```

クラス名の設定で .block-{値}のように配列の値に応じてクラス名を動的に設定しています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/601137/ac0f123a-9a8e-c668-4518-27317ab283f5.png)

## Tetrominoクラス作成
テトリミノは自分がフィールド上のどこにいるのか、と自分が持つブロックの配置を定義します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/601137/6a7a8ead-8a19-873e-d785-45e1e11a5531.png)

下記Tetrominoクラスのblocksは画像でいうと青色のT字テトリミノを表現しています。
4つのブロックを持つので(0,0)を原点とした4つの位置情報を配列の中に含みます。

フィールド上に描画されとる時の位置は(5,5)の位置です
```ts:src/common/tetromino.ts
export class Tetromino {
  id = 1
  blocks = [
    [0, 0],
    [0, -1],
    [0, 1],
    [-1, 0]
  ]
  fieldPosition = {
    x: 5,
    y: 5
  }
}
```

上記の位置情報からフィールド上に描画する際の配列を作成する事ができます
```ts:src/common/tetromino.ts
  get blocksOnField() {
    // blocksをフィールド上の位置に対応させたい
    const blocksOnField = []

    // blocksの各位置情報にフィールド上の位置x,yを追加する
    for (const block of this.blocks) {
      const positionOnField = [block[0] + this.fieldPosition.y, block[1] + this.fieldPosition.x]
      blocksOnField.push(positionOnField)
    }
    return blocksOnField
  }
```

例えば画像のT字型のテトリミノだとフィールド上(9,4)に描画されるるため、各位置情報に(9,4)を加えて下記のように求められます。
(0,0)　→　(9,4)
(0,-1)　→　(9,3) 
(-1,0)　→　(8,3)
(0,1)　→　(9,5)
フィールド上の描画位置とブロックの配置を足し算しています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/601137/a558737b-eecd-9e4a-951b-faaec680dcc9.png)


## フィールドにテトリミノを描画する
フィールドクラス側ではテトリミノクラスを受け取り、自身の描画用のフィールド配列を更新するメソッドを実装します。
初期状態では配列の中身は全て0ですが、下記のメソッドでテトリミノの位置、ブロック配置を取得し対応するフィールド配列の値をテトリミノのID(T字のテトリミノはID=1)に書き換えます
```ts:src/common/tetromino.ts
// 描画用フィールドに与えられたテトリミノを描画する関数
 renderTetromino(tetromino: TetrominoQiita) {
    const blocks = tetromino.blocksOnField
    blocks.forEach((block) => {
      // block[0]はy、block[1]はxの値
      // blockはフィールド上のテトリミノブロックの位置を表す
      this.renderField[block[0]][block[1]] = tetromino.id
    })
  }
```

PlayGame.vue側でインスタンス化して下記のように使用してみます。
```vuejs:src/pages/PlayGame.vue
<script setup lang="ts">
const field = reactive(new FieldQiita());
const tetromino = new TetrominoQiita();

// インスタンス化したテトリミノを描画する
field.renderTetromino(tetromino);
</script>
```
上記でフィールド配列に描画されたテトリミノはクラスフィールドでid=1が定義されていたので、フィールド描画用の配列の対応する位置が1で上書きされます。

画面デザインを整えるためにcssでid=1のブロックに対するデザインを定義します。
```scss
<styles lang="scss">
...
  .block {
    &-0 {
      background: #ccc;
    }

    &-1 {
      background: #3498db;
    }
  }
...
</style>
```
http://localhost:4000/play で画面を確認すると、フィールド配列の数字がテトリミノを描画した部分に対応して1に書き換わっている事が確認できます。

描画位置は固定値でx:5, y:5としたので、その位置を中心とした各ブロック位置にしたがって描画できていると思います。
またデザインもクラス名に含まれるテトリミノのIDに対応したcssを定義したので青くなっています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/601137/3db3b01d-0709-9282-4b39-5c4d35aa54b9.png)

ここまででフィールドにテトリミノを描画することができました。

## 次回の内容
フィールド上にテトリミノを配置する事ができたので、次回はテトリミノの移動や回転を実装していきたいと思います。
せっかくクラス定義しているのでテトリミノの振る舞いはテトリミノクラスに関数として定義し自身の持つ状態を変更する事で実現してきたいと思います。