Nuxt.jsで15パズルを作ってGitHub Pagesで公開する

# はじめに
Nuxt.jsを使って15パズルを作成してみました。本稿では作成手順とGitHub Pagesへの公開方法を説明します。最終的に↓こんなのができます。
![136709856-cfd2099e-e6ac-4b30-a441-23b44a35a2dd.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2087075/9df203eb-0b04-e798-503f-5cfd5bfc952d.gif)

ソースコード
https://github.com/jeronimo34/15puzzle_nuxtjs

# 環境
Windows 10 Home

# 前提
Node.js
npm
git
などインストール済み

# プロジェクトの作成

以下コマンドを実行しプロジェクトを作成します。

``` 
npm init nuxt-app 15puzzle
```

プロジェクトの初期設定をしていきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2087075/36c89db2-f198-acbf-adcb-d7660058e480.png)

以下の2つに関しては次に示す値を設定します。それ以外はEnterを押します。
Rendering mode = Single Page App
Deployment target = Static

初期設定が完了したら動作確認をしてみます。以下のコマンドを実行します。

```
cd 15puzzle
npm run dev
```

以下のURLをブラウザで開きNuxt.jsの初期画面が表示されることを確認します。
http://localhost:3000/
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2087075/f1a4c105-6331-95bb-8cf1-b13fa8b700d4.png)

これでアプリの雛形が完成しました。

# 実装
実装していきます。
こちらの記事を参考にさせて頂きました。

https://qiita.com/dorarep/items/b9783ea6cd18308a06a0

## 不要なファイルの削除
まず使用しないファイルを消します。
/components/Tutorial.vueを削除します。

## ファイルの追加
以下のファイルを追加します。

|ファイル名|説明|
|---|---|
|/pages/index.vue|ビュー|
|/components/GameFrame.vue|「クリア！」などのメッセージ表示|
|/components/GameBoard.vue|パズル全体|
|/components/Piece.vue|パズルのピース|
|/store/game.js|ゲームのロジック|

###コンポーネントの図解
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2087075/b709ea07-ae79-b6b2-7009-2008692c074c.png)

各ファイルに以下の内容をコピペします。

```vue:pages/index.vue
<template>
  <div>
    <h1>15パズル</h1>
    <section id="container">
      <GameFrame/>
    </section>
  </div>
</template>

<script>
  import GameFrame from '~/components/GameFrame.vue'
  export default {
    components: {
      GameFrame
    }
  }
</script>

<style>
html {
  font-family: "Source Sans Pro", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  font-size: 28px;
  word-spacing: 1px;
  -ms-text-size-adjust: 100%;
  -webkit-text-size-adjust: 100%;
  -moz-osx-font-smoothing: grayscale;
  -webkit-font-smoothing: antialiased;
  box-sizing: border-box;
  
}
*, *:before, *:after {
  box-sizing: border-box;
  margin: 0;
}
html, body {margin: 0; height: 100%; overflow: hidden}
h1{
    text-align: center;
    margin-top:10px;
    margin-bottom:30px;
}
</style>
```

```vue:components/GameFrame.vue
<template>
    <div>
        <GameBoard/>
        <div class="clearArea">
            <p v-if="$store.state.game.isClear">クリア!</p>
            <button v-if="$store.state.game.isClear" v-on:click="resetGame()">リトライ</button>
        </div>
    </div>
</template>

<script>
  import GameBoard from '~/components/GameBoard.vue'
  export default {
    components: {
      GameBoard
    },
    methods: {
        resetGame(){
            this.$store.commit('game/resetGame')
        }
    }
  }
</script>

<style scoped>
  .clearArea{
    text-align: center;
  }
  .clearArea p {
      margin:10px;
  }
  .clearArea button {
      margin:10px;
  }
  
</style>
```

```vue:components/GameBoard.vue
<template>
    <div class="gameboard">
        <Piece 
            v-for="piece in pieces" 
            :key="piece.pos" 
            :number="piece.number" 
            :y="Math.floor(piece.pos/4)"
            :x="Math.floor(piece.pos%4)"
        />
    </div>
</template>

<script>
  import Piece from '~/components/Piece.vue'
  export default {
    computed: {
        pieces(){
            return this.$store.state.game.pieces;
        }
    },
    components: {
      Piece
    }
  }

</script>

<style scoped>
  .gameboard{
    position: relative;
    width: 256px;
    height: 256px;
    margin: auto;
  }
</style>
```

```vue:components/Piece.vue
<template>
    <div class="piece"
        v-if="number!==16" @click="movePiece(x,y)" :style="{top:`${y*64}px`, left:`${x*64}px`}">
        <span>{{number}}</span>
    </div>
    <div 
        class="blank"
        v-else @click="movePiece(x,y)" :style="{top:`${y*64}px`, left:`${x*64}px`}">
        <span>{{number}}</span>
    </div>
</template>

<script>
  export default {
    props: ['number','y', 'x'],
    methods: {
        movePiece (x, y) {
            this.$store.commit('game/movePiece', {x, y})
        }
    }
  }

</script>

<style scoped>
  .piece{
    height: 64px;
    width: 64px;
    border: ridge 1px #fff;
    position: absolute;
    background: lightblue;
    cursor: pointer;
    color:#666666;
  }
  .piece:hover{
    opacity:0.3;
    color:white;
  }
  .blank{
    height: 64px;
    width: 64px;
    border: ridge 1px #fff;
    position: absolute;
    background: lightyellow;
    cursor: default;
    color:#666666;
  }
  span{
    top: 50%;
    left: 50%;
    position: absolute;
    transform: translate(-50%, -50%);
  }
</style>
```

```javascript:store/game.js
const BOARD_X = 4
const BOARD_Y = 4
const BLANK = 16;

// 初期状態
const initialState = {
  // ピースの初期位置
  pieces: [
    {pos:0, number:2},
    {pos:1, number:9},
    {pos:2, number:3},
    {pos:3, number:4},
    {pos:4, number:7},
    {pos:5, number:10},
    {pos:6, number:6},
    {pos:7, number:8},
    {pos:8, number:13},
    {pos:9, number:1},
    {pos:10, number:16},
    {pos:11, number:15},
    {pos:12, number:14},
    {pos:13, number:5},
    {pos:14, number:12},
    {pos:15, number:11}, 
  ],
  // クリアフラグ
  isClear: false
}

// 状態初期化
export const state = () => JSON.parse(JSON.stringify(initialState))

// 状態リセット
function resetState(state){
    Object.assign(state, JSON.parse(JSON.stringify(initialState)))
}

export const mutations = {
    // リセットボタン押下時処理
    resetGame (state) {
        resetState(state);
    },
    // ピースの移動処理
    movePiece(state, {x,y}){
        // クリア済みならピースを移動させない
        if(state.isClear) return;

        //　ピースの移動処理開始
        let pos = y * 4 + x;
        let clickedPiece = state.pieces.find((x)=>x.pos === pos);

        //　クリックしたピースと、空白のマスの位置を入れ替える
        let dx = [1,0,-1,0];
        let dy = [0,1,0,-1];
        for(let i = 0; i < 4; ++i){
            let tx = x + dx[i];
            let ty = y + dy[i];
            if(tx < 0 || BOARD_X <= tx) continue;
            if(ty < 0 || BOARD_Y <= ty) continue;
            
            let newPos = ty * BOARD_X + tx;
            let targetPiece = state.pieces.find((x)=>x.pos === newPos && x.number === BLANK);

            if(targetPiece === undefined) continue;

            let tmpNumber = clickedPiece.number;
            clickedPiece.number = targetPiece.number;
            targetPiece.number = tmpNumber;

            break;
        }

        // クリア判定
        state.isClear = state.pieces.every(x => x.pos === x.number-1);
    }
}

```


以下のコマンドを実行し、動作を確認します。

```
npm run dev
```

# GitHub Pagesへの公開
以下の内容を実行していきます。
https://nuxtjs.org/ja/deployments/github-pages#deploy-nuxt-on-github-pages

GitHubにレポジトリを作成します。

ローカルレポジトリに変更をコミットします。

```
git add -A
git commit -m "first commit"
```

nuxt.config.jsにレポジトリ名を設定します。

```nuxt.config.js
  // GitHub Pagesにデプロイしたとき正常に動作させるための設定
  router: {
    base: '/<レポジトリ名>/'
  },
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2087075/0e5dc320-572f-fe0a-e75c-fed621332d76.png)

以下のコマンドを実行します。

```
npm install push-dir --save-dev
```

package.jsonに以下を追記します。

```
"scripts": {
  "dev": "nuxt",
  "generate": "nuxt generate",
  "start": "nuxt start",
  "deploy": "push-dir --dir=dist --branch=gh-pages --cleanup"
},
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2087075/9375eea4-31d0-bea4-e196-7b42d7c7f50b.png)

以下のコマンドを実行し、GitHub Pagesに成果物をデプロイします。

```
npm run generate
npm run deploy
```

以下のURLをブラウザで開き公開されていることを確認します。
https://<ユーザ名>.github.io/<レポジトリ名>/
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2087075/d6c92890-6abd-8224-96a4-ea58642056ed.png)

作業は以上です。お疲れ様でした。

# おわりに
Nuxt.jsは開発を容易にする機能やレンダリングモードの切り替えなど、様々な機能を提供しています。

今回作成したアプリは単一のページ、かつ、機能も少なかったので、Nuxt.jsの機能を全て試してみることはできませんでしたが、ディレクトリ構造が決められていたり、GitHub Pagesに成果物を公開する手段が用意されていたりするところは便利だと思いました。


# 参考
https://qiita.com/dorarep/items/b9783ea6cd18308a06a0
https://nuxtjs.org/ja/docs/get-started/installation
https://nuxtjs.org/ja/deployments/github-pages/#deploy-nuxt-on-github-pages