Vue×TypeScript×AWS Amplifyでリバーシ作成

# Vue×TypeScriptでリバーシの作成とAWS Amplifyでの公開
Vue2とTypeScriptを使用してオセロを作成し、Amplifyで公開してみました

AWS Amplifyは、フロントエンドのデベロッパーが AWS でフルスタックアプリケーションをすばやく簡単に構築できるようにする専用のツールとサービスのセットです。Amplify では、ウェブアプリケーション用のJavaScript、React、Angular、Vue、Next.js、モバイルアプリケーション用の Android、iOS、React Native、Ionic、Flutter などの一般的な言語、フレームワーク、およびプラットフォームがサポートされています。


## 概要
Vue CLIから`vue create reversi`でプロジェクトを作成。

構成はこんな感じです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1583139/fe6ddee1-da48-acbb-758d-8d195232d6b8.png)



Main.vueが最初の画面で、ゲーム開始を押すとVGameコンポーネントンに移ります。

```js
<button>
  <router-link to="game">ゲーム開始</router-link>
</button>
```
VGameコンポーネントではBoardコンポーネントを呼び出しています。コンポーネントの親子関係はVGame > Board > Row > Cell　になります。
![キャプチャ.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1583139/48db3008-fda5-1fc3-e1b6-e007e6ccc17d.png)


VGameコンポーネントでは黒と白どちらのターンなのかを管理し、子コンポーネントに渡しています。Cellでその値を受け取り、そのターンの色の石を置くことができます。石を置いたらターンを変え、それをemitしてVGameに返します。

石はすべてのマス(cell)に透明な石がすでに置いてあるイメージで、クリックして石を置くとblackまたはwhiteのcssのクラスが適用されます。

modelフォルダにはboard, row, cellのクラスを用意し、cellが入る配列をboardコンポーネントで用意します。cellクラスはx,yを座標として持ちこれを石を置いたときの判定に利用してます。

```js
export class Board {
  public rows: Row[]
  
  constructor() {
//Rowクラスの入った配列を用意
this.rows = [...Array(8).keys()].map(i => new Row(i))
//ゲーム開始時に置かれた4つの石
    this.rows[3].cells[3].state = CellState.White
    this.rows[3].cells[4].state = CellState.Black
    this.rows[4].cells[3].state = CellState.Black
    this.rows[4].cells[4].state = CellState.White
  }
}

export class Row {
  public cells: Cell[]
//cellのy座標になる
  public rowNumber: number
  
  constructor(rowNumber: number) {
    this.rowNumber = rowNumber
//Rowクラスの配列の中にCellクラスが8つ入っている
    this.cells = [...Array(8).keys()].map(i => new Cell(i, rowNumber))
  }
}

export class Cell {
  public x: number
  public y: number
  public state: CellState = CellState.None

  constructor(x: number, y: number) {
    this.x = x
    this.y = y
  }
//もっと続く・・・
```
石を置けるかどうかの判定はcellクラスで行っており、石を置ける場合は、ひっくり返すことのできる石の座標を配列に入れ、置けない場合はfalseを返します。これをCell.vueのmethodsで石が置けるか確認し、置ける場合は配列に入った座標の石を今のターンの色の変えるように処理しています。

```js
  public checkRightStone(row: Cell[], color: CellState, checkedCell: string[]): boolean | string[] {
    //右端に置いた場合
    if (this.x === 7) {
      return checkedCell
    }
    const rightCell = row[this.x + 1]
    //右隣に石がない
    if (rightCell.state === 'None') {
      return false
    }
    //右隣が同じ色
    if (color === rightCell.state ) {
      checkedCell.push(`${rightCell.x}${rightCell.y}`)
      return checkedCell
    }
    //1つ右隣が自分と違う色かつ石が置いてある
    if (color !== rightCell.state) {
      checkedCell.push(`${rightCell.x}${rightCell.y}`)
      //さらにもう一つ右隣を調査
      if (!rightCell.checkRightStone(row, color, checkedCell)) {
        return false
      }
      rightCell.checkRightStone(row, color, checkedCell)
      return checkedCell
    }
    return false
  }
```   

判定して置ける場合はVue.cellコンポーネントのmethodsにある関数ででクラスを追加してます。

<details><summary>Vue.cell</summary><div>

```js
<template>
  <div :class="cellxy" class="cellSize" @click="putStone()">
    <div class="cell"></div>
    <div class="stone"></div>
  </div>
</template>

<script lang="ts">
import Vue from "vue"
import { Board, Cell, CellState } from "@/model/reversi"

export default Vue.extend({
  props: ["cell", "turn", "reversirow", "reversiboard"],
  data() {
    return {
      reversicell: this.cell as Cell,
      stoneState: this.cell.state as string,
      cellxy: `cell${this.cell.x}${this.cell.y}`,
      playerTurn: this.turn as string,
      row: this.reversirow.cells,
      boardRows: this.reversiboard as Board,
      willBeChangedSideStone: [] as string[],
      willBeChangedUpDownStone: [] as string[],
      willBeChangedSharpeStone: [] as string[]
    }
  },
  mounted() {
    this.addColorClass(`.${this.cellxy}`)
  },
  methods: {
    addColorClass(point: string) {
      this.removeClass(point)
      const cellBox = document.querySelector(point)! as HTMLDivElement
      if (this.stoneState === CellState.None) return
      cellBox.querySelector(".stone")!.classList.add(this.stoneState)
    },
    removeClass(point: string) {
      //石を挟んで色を変えるときクラスを一度削除する
      const cellBox = document.querySelector(point)! as HTMLElement
      if (
        cellBox
          .querySelector(".stone")!
          .classList.contains(CellState.Black || CellState.White)
      ) {
        cellBox
          .querySelector(".stone")!
          .classList.remove(
            CellState.Black || CellState.White || CellState.None
          )
      }
      return
    },
    putStone() {
      if (this.stoneState === CellState.None) {
        this.stoneState = this.turn
        if (this.checkAllWay()) {
          this.playerTurn = this.turn
          this.reversicell.state = this.turn

          this.addColorClass(`.${this.cellxy}`)
          this.changeSideStones()
          this.changeUpDownStones()
          this.changeSharpeStones()
          this.playerTurn === CellState.White
            ? (this.playerTurn = CellState.Black)
            : (this.playerTurn = CellState.White)
          this.$emit("putStone", this.playerTurn)
        }
      }
    },
    checkAllWay() {
      const result1 = this.checkCanPutRight()
      const result2 = this.checkCanPutLeft()
      const result3 = this.checkCanPutUpper()
      const result4 = this.checkCanPutBottom()
      const result5 = this.checkCanPutRightUpper()
      const result6 = this.checkCanPutLeftUpper()
      const result7 = this.checkCanPutRightBottom()
      const result8 = this.checkCanPutLeftBottom()
      if (
        result1 ||
        result2 ||
        result3 ||
        result4 ||
        result5 ||
        result6 ||
        result7 ||
        result8
      ) {
        return true
      } else return false
    },
    changeSideStones() {
      if (this.willBeChangedSideStone.length < 2) return
      for (const xy of this.willBeChangedSideStone) {
        const x = Number(xy.split("")[0])
        const y = Number(xy.split("")[1])
        this.row[x].state = this.turn
        this.addColorClass(`.cell${x}${y}`)
      }
      return
    },
    changeUpDownStones() {
      if (this.willBeChangedUpDownStone.length < 2) return
      for (const xy of this.willBeChangedUpDownStone) {
        const x = Number(xy.split("")[0])
        const y = Number(xy.split("")[1])
        this.boardRows.rows[y].cells[x].state = this.turn
        this.addColorClass(`.cell${x}${y}`)
      }
      return
    },
    changeSharpeStones() {
      if (this.willBeChangedSharpeStone.length < 2) return
      for (const xy of this.willBeChangedSharpeStone) {
        const x = Number(xy.split("")[0])
        const y = Number(xy.split("")[1])
        this.boardRows.rows[y].cells[x].state = this.turn
        this.addColorClass(`.cell${x}${y}`)
      }
      return
    },
    checkCanPutRight() {
      const checkCells: string[] = []
      const checked = this.cell.checkRightStone(this.row, this.turn, checkCells)
      //falseまたは配列の要素が一つの時は石がおけない
      //要素が一つということは、置こうとしている石と同じ色の石が1つ右にある
      if (!checked || checked.length <= 1) return false
      else {
        this.willBeChangedSideStone.push(...checked)
        return true
      }
    },
    checkCanPutLeft() {
      const checkCells: string[] = []
      const checked = this.cell.checkLeftStone(this.row, this.turn, checkCells)

      if (!checked || checked.length <= 1) return false
      else {
        this.willBeChangedSideStone.push(...checked)
        return true
      }
    },
    checkCanPutUpper() {
      const checkCells: string[] = []
      const checked = this.cell.checkUpperStone(
        this.boardRows,
        this.turn,
        checkCells
      )
      if (!checked || checked.length <= 1) return false
      else {
        this.willBeChangedUpDownStone.push(...checked)
        return true
      }
    },
    checkCanPutBottom() {
      const checkCells: string[] = []
      const checked = this.cell.checkBottomStone(
        this.boardRows,
        this.turn,
        checkCells
      )
      if (!checked || checked.length <= 1) return false
      else {
        this.willBeChangedUpDownStone.push(...checked)
        return true
      }
    },
    checkCanPutRightUpper() {
      const checkCells: string[] = []
      const checked = this.cell.checkRightUpperStone(
        this.boardRows,
        this.turn,
        checkCells
      )
      if (!checked || checked.length <= 1) return false
      else {
        this.willBeChangedSharpeStone.push(...checked)
        return true
      }
    },
    checkCanPutLeftUpper() {
      const checkCells: string[] = []
      const checked = this.cell.checkLeftUpperStone(
        this.boardRows,
        this.turn,
        checkCells
      )
      if (!checked || checked.length <= 1) return false
      else {
        this.willBeChangedSharpeStone.push(...checked)
        return true
      }
    },
    checkCanPutRightBottom() {
      const checkCells: string[] = []
      const checked = this.cell.checkRightBottomStone(
        this.boardRows,
        this.turn,
        checkCells
      )
      if (!checked || checked.length <= 1) return false
      else {
        this.willBeChangedSharpeStone.push(...checked)
        return true
      }
    },
    checkCanPutLeftBottom() {
      const checkCells: string[] = []
      const checked = this.cell.checkLeftBottomStone(
        this.boardRows,
        this.turn,
        checkCells
      )
      if (!checked || checked.length <= 1) return false
      else {
        this.willBeChangedSharpeStone.push(...checked)
        return true
      }
    }
  }
})
</script>

<style scoped>
.cellSize {
  height: 60px;
}
.cell {
  background-color: rgb(46, 141, 46);
  border: solid 2px #000;
  width: 60px;
  height: 60px;
}
.stone {
  position: relative;
  top: -60px;
  width: 50px;
  height: 50px;
  border-radius: 50%;
  content: "";
  margin: 4px auto;
  z-index: 1;
}
.white {
  background-color: #fff;
}
.black {
  background-color: #000;
}
</style>
```

</div>
</details>

<details><summary>Row.vue</summary><div>

```js
<template>
  <div id="row">
    <Cell
      v-for="cell in reversirow.cells"
      :key="`${cell.x}-${cell.y}`"
      :cell="cell"
      :turn="turn"
      :reversirow="reversirow"
      :reversiboard="reversiboard"
      @putStone="takeTurn($event)"
    />
  </div>
</template>

<script lang="ts">
import Vue from "vue"
import Cell from "@/components/reversi/Cell.vue"

export default Vue.extend({
  props: ["row", "turn", "reversiboard"],
  data() {
    return {
      reversirow: this.row
    }
  },
  components: {
    Cell
  },
  methods: {
    takeTurn(playerTurn: "black" | "white") {
      this.$emit("putStone", playerTurn)
    }
  }
})
</script>

<style scoped>
#row {
  display: flex;
  flex-direction: row;
  justify-content: center;
}
</style>
```

</div>
</details>

<details><summary>Board.vue</summary><div>

```js
<template>
  <div id="board">
    <Row
      v-for="row in reversiboard.rows"
      :key="row.rowNumber"
      :row="row"
      :turn="turn"
      :reversiboard="reversiboard"
      @putStone="switchTurn($event)"
    />
  </div>
</template>

<script lang="ts">
import Vue from "vue"
import Row from "@/components/reversi/Row.vue"

export default Vue.extend({
  props: ["board", "turn"],
  data() {
    return {
      reversiboard: this.board
    }
  },
  components: {
    Row
  },
  methods: {
    switchTurn(playerTurn: "black" | "white") {
      this.$emit("putStone", playerTurn)
    }
  }
})
</script>

<style scoped>
#board {
  display: flex;
  flex-direction: column;
  justify-content: center;
  margin: 30px auto;
}
</style>
```

</div>
</details>

<details><summary>VGame.vue</summary><div>

```js
<template>
  <div class="game">
    <h3>{{ whosTurn(turn) }}のターン</h3>
    <Board :board="board" :turn="turn" @putStone="whosTurn($event)" />
  </div>
</template>

<script lang="ts">
import Vue from "vue"
import Board from "@/components/reversi/Board.vue"
import { Board as BoardModel } from "@/model/reversi"

export default Vue.extend({
  data() {
    return {
      board: new BoardModel(),
      turn: "black"
    }
  },
  components: {
    Board
  },
  methods: {
    whosTurn(playerTurn: "black" | "white") {
      this.turn = playerTurn
      return playerTurn === "black" ? "黒" : "白"
    }
  }
})
</script>

<style scoped>
.game {
  margin: 0 auto;
}
</style>
```

</div>
</details>

<details><summary>model/reversi.ts</summary><div>

```js
export class Board {
  public rows: Row[];

  constructor() {
    this.rows = [...Array(8).keys()].map((i) => new Row(i));
    this.rows[3].cells[3].state = CellState.White;
    this.rows[3].cells[4].state = CellState.Black;
    this.rows[4].cells[3].state = CellState.Black;
    this.rows[4].cells[4].state = CellState.White;
  }
}

export class Row {
  public cells: Cell[];
  public rowNumber: number;

  constructor(rowNumber: number) {
    this.rowNumber = rowNumber;
    this.cells = [...Array(8).keys()].map((i) => new Cell(i, rowNumber));
  }
}

export class Cell {
  public x: number;
  public y: number;
  public state: CellState = CellState.None;

  constructor(x: number, y: number) {
    this.x = x;
    this.y = y;
  }

  public checkRightStone(
    row: Cell[],
    color: CellState,
    checkedCell: string[]
  ): boolean | string[] {
    //右端に置いた場合
    if (this.x === 7) {
      return checkedCell;
    }
    const rightCell = row[this.x + 1];
    //右隣に石がない
    if (rightCell.state === CellState.None) {
      return false;
    }
    //右隣が同じ色
    if (color === rightCell.state) {
      checkedCell.push(`${rightCell.x}${rightCell.y}`);
      return checkedCell;
    }
    //1つ右隣が自分と違う色かつ石が置いてある
    if (color !== rightCell.state) {
      checkedCell.push(`${rightCell.x}${rightCell.y}`);
      //さらにもう一つ右隣を調査
      if (!rightCell.checkRightStone(row, color, checkedCell)) {
        return false;
      }
      rightCell.checkRightStone(row, color, checkedCell);
      return checkedCell;
    }
    return false;
  }
  public checkLeftStone(
    row: Cell[],
    color: CellState,
    checkedCell: string[]
  ): boolean | string[] {
    if (this.x === 0) {
      return checkedCell;
    }
    const leftCell = row[this.x - 1];
    if (leftCell.state === CellState.None) {
      return false;
    }
    if (color === leftCell.state) {
      checkedCell.push(`${leftCell.x}${leftCell.y}`);
      return checkedCell;
    }
    if (color !== leftCell.state) {
      checkedCell.push(`${leftCell.x}${leftCell.y}`);
      if (!leftCell.checkLeftStone(row, color, checkedCell)) {
        return false;
      }
      leftCell.checkLeftStone(row, color, checkedCell);
      return checkedCell;
    }
    return false;
  }
  public checkUpperStone(row: Board, color: CellState, checkedCell: string[]): boolean | string[] {
    if (this.y === 0) {
      return checkedCell;
    }
    const upperCell = row.rows[this.y - 1].cells[this.x];
    if (upperCell.state === CellState.None) {
      return false;
    }
    if (color === upperCell.state) {
      checkedCell.push(`${upperCell.x}${upperCell.y}`);
      return checkedCell;
    }
    if (color !== upperCell.state) {
      checkedCell.push(`${upperCell.x}${upperCell.y}`);
      if (!upperCell.checkUpperStone(row, color, checkedCell)) {
        return false;
      }
      upperCell.checkUpperStone(row, color, checkedCell);
      return checkedCell;
    }
    return false;
  }
  public checkBottomStone(row: Board, color: CellState, checkedCell: string[]): boolean | string[] {
    if (this.y === 7) {
      return checkedCell;
    }
    const upperCell = row.rows[this.y + 1].cells[this.x];
    if (upperCell.state === CellState.None) {
      return false;
    }
    if (color === upperCell.state) {
      checkedCell.push(`${upperCell.x}${upperCell.y}`);
      return checkedCell;
    }
    if (color !== upperCell.state) {
      checkedCell.push(`${upperCell.x}${upperCell.y}`);
      if (!upperCell.checkBottomStone(row, color, checkedCell)) {
        return false;
      }
      upperCell.checkBottomStone(row, color, checkedCell);
      return checkedCell;
    }
    return false;
  }
  public checkRightUpperStone(
    row: Board,
    color: CellState,
    checkedCell: string[]
  ): boolean | string[] {
    if (this.y === 0 || this.x === 7) {
      return checkedCell;
    }
    const RightUpperCell = row.rows[this.y - 1].cells[this.x + 1];
    if (RightUpperCell.state === CellState.None) {
      return false;
    }
    if (color === RightUpperCell.state) {
      checkedCell.push(`${RightUpperCell.x}${RightUpperCell.y}`);
      return checkedCell;
    }
    if (color !== RightUpperCell.state) {
      checkedCell.push(`${RightUpperCell.x}${RightUpperCell.y}`);
      if (!RightUpperCell.checkRightUpperStone(row, color, checkedCell)) {
        return false;
      }
      RightUpperCell.checkRightUpperStone(row, color, checkedCell);
      return checkedCell;
    }
    return false;
  }
  public checkLeftUpperStone(
    row: Board,
    color: CellState,
    checkedCell: string[]
  ): boolean | string[] {
    if (this.y === 0 || this.x === 0) {
      return checkedCell;
    }
    const leftUpperCell = row.rows[this.y - 1].cells[this.x - 1];
    if (leftUpperCell.state === CellState.None) {
      return false;
    }
    if (color === leftUpperCell.state) {
      checkedCell.push(`${leftUpperCell.x}${leftUpperCell.y}`);
      return checkedCell;
    }
    if (color !== leftUpperCell.state) {
      checkedCell.push(`${leftUpperCell.x}${leftUpperCell.y}`);
      if (!leftUpperCell.checkLeftUpperStone(row, color, checkedCell)) {
        return false;
      }
      leftUpperCell.checkLeftUpperStone(row, color, checkedCell);
      return checkedCell;
    }
    return false;
  }
  public checkRightBottomStone(
    row: Board,
    color: CellState,
    checkedCell: string[]
  ): boolean | string[] {
    if (this.y === 7 || this.x === 7) {
      return checkedCell;
    }
    const RightBottomCell = row.rows[this.y + 1].cells[this.x + 1];
    if (RightBottomCell.state === CellState.None) {
      return false;
    }
    if (color === RightBottomCell.state) {
      checkedCell.push(`${RightBottomCell.x}${RightBottomCell.y}`);
      return checkedCell;
    }
    if (color !== RightBottomCell.state) {
      checkedCell.push(`${RightBottomCell.x}${RightBottomCell.y}`);
      if (!RightBottomCell.checkRightBottomStone(row, color, checkedCell)) {
        return false;
      }
      RightBottomCell.checkRightBottomStone(row, color, checkedCell);
      return checkedCell;
    }
    return false;
  }
  public checkLeftBottomStone(
    row: Board,
    color: CellState,
    checkedCell: string[]
  ): boolean | string[] {
    if (this.y === 7 || this.x === 0) {
      return checkedCell;
    }
    const LeftBottomCell = row.rows[this.y + 1].cells[this.x - 1];
    if (LeftBottomCell.state === CellState.None) {
      return false;
    }
    if (color === LeftBottomCell.state) {
      checkedCell.push(`${LeftBottomCell.x}${LeftBottomCell.y}`);
      return checkedCell;
    }
    if (color !== LeftBottomCell.state) {
      checkedCell.push(`${LeftBottomCell.x}${LeftBottomCell.y}`);
      if (!LeftBottomCell.checkLeftBottomStone(row, color, checkedCell)) {
        return false;
      }
      LeftBottomCell.checkLeftBottomStone(row, color, checkedCell);
      return checkedCell;
    }
    return false;
  }
}

export enum CellState {
  White = "white",
  Black = "black",
  None = "None",
}

```

</div>
</details>

## AWS Amplifyで公開する
amplify cliをインストールして、設定をします。

```
npm install -g @aws-amplify/cli
amplify -v
7.3.5
amplify configure
```

次にAmplifyのプロジェクトを立ち上げて初期化します。プロジェクトを初期化するには次のコマンドを実行する。
必要な情報に応えると`amplify`ディレクトリと`src/aws-exports.js`ファイルが作成されます。

```
amplify init

? Initialize the project with the above configuration? No
? Enter a name for the environment dev
? Choose your default editor: Visual Studio Code
? Choose the type of app that you're building javascript
Please tell us about your project
? What javascript framework are you using vue
? Source Directory Path:  src
? Distribution Directory Path: dist
? Build Command:  npm run build
? Start Command: npm start
Using default provider  awscloudformation
? Select the authentication method you want to use: AWS access keys
? accessKeyId:  ********************
? secretAccessKey:  ****************************************
? region:  us-east-1
```

`amplify add hosting`でS3での静的ウェブホスティングを有効にします。ホスティングを有効にし、`amplify publish`を実行して、ビルド＆デプロイが終わるとCloudFrontで公開されます。

```
amplify add hosting
? Select the plugin module to execute Amazon CloudFront and S3
? Select the environment setup: PROD (S3 with CloudFront using HTTPS)
? hosting bucket name vuetsreversi-20211117151455-hostingbucket

amplify publish
✔ Uploaded files successfully.
Your app is published successfully.
https://xxxxxxxxxx.cloudfront.net
```

https://github.com/aws-amplify/amplify-cli

## つまずいたところ
・Vueはpropsとemitでデータをうまく渡せず苦戦しました。VGame.vue -> Board.vue ->Row.vue -> Cell.vueの流れでVGameからCellに渡してるので、Vuexを使ってもっとスマートにできたらよかったです。

・computedを使わずにmethodsにばかり処理を記述してしまった。

・Vue CLIはESLint8.0を現時点ではサポートしていないようで、ビルドや`amplify publish`でエラーが出てしまいました。ESLintのバージョンを`“eslint”: “^7.0.0"`にダウングレードしたところうまくいきました。
修正はこちら↓のコメントを参考にさせていただきました。

https://github.com/vuejs/vue-cli/issues/6759


## 感想
Vue,TypeScriptの勉強よりロジック考えるのが大変でした。(まだバグが結構ありますが、、、)
TypeScriptで勉強になったのはenumを使ったことくらいかもしれません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1583139/48c59de5-ca41-aa1e-c3e3-dda6b7378784.png)

あとは、もう少しきれいにコード書けるようになりたいです( ;∀;)


出だしはこちらの動画を参考にしました。動画ではvuetifyを使って作っています。

https://www.youtube.com/watch?v=RNr3AsZZ9SQ