1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2021-12-04

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

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

<button>
  <router-link to="game">ゲーム開始</router-link>
</button>

VGameコンポーネントではBoardコンポーネントを呼び出しています。コンポーネントの親子関係はVGame > Board > Row > Cell になります。
キャプチャ.PNG

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

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

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

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で石が置けるか確認し、置ける場合は配列に入った座標の石を今のターンの色の変えるように処理しています。

  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にある関数ででクラスを追加してます。

Vue.cell
<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>
Row.vue
<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>
Board.vue
<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>
VGame.vue
<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>
model/reversi.ts
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",
}

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

つまずいたところ

・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"にダウングレードしたところうまくいきました。
修正はこちら↓のコメントを参考にさせていただきました。

感想

Vue,TypeScriptの勉強よりロジック考えるのが大変でした。(まだバグが結構ありますが、、、)
TypeScriptで勉強になったのはenumを使ったことくらいかもしれません。
image.png

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

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

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?