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
でプロジェクトを作成。
Main.vueが最初の画面で、ゲーム開始を押すとVGameコンポーネントンに移ります。
<button>
<router-link to="game">ゲーム開始</router-link>
</button>
VGameコンポーネントではBoardコンポーネントを呼び出しています。コンポーネントの親子関係はVGame > Board > Row > Cell になります。
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を使ったことくらいかもしれません。
あとは、もう少しきれいにコード書けるようになりたいです( ;∀;)
出だしはこちらの動画を参考にしました。動画ではvuetifyを使って作っています。