すぐできる!Vueで○×ゲームをつくってみよう

さてさて、このブログでは過去に反射神経ゲームタイピングゲームなど、Vueを使ったちょっとしたゲームの作り方を公開してきました。

そして、最近Google Analyticsを見てみると、これらゲーム記事の滞在時間は他と比べると長めになっているのでより皆さんに喜んでいただけているようです。^^

ただ、そうはいってもゲーム開発となると見た目はシンプルでもプログラム的にはコードが複雑だったりするので、主にVueビギナーの方々へ記事をお届けしたい私からするとどのような記事がいいか少し悩ましかったりもします。

そこで!

今回は、シンプルだけど比較的簡単にプログラムをつくれるであろう「○×ゲーム」をVueでつくる記事をお届けすることにしました。

ぜひ学習に役立ててくださいね。
(最後に教材ソースコードをダウンロードできます)

※ 開発環境: Vue 2.5

やりたいこと

子供の頃よく友達と遊んだ次のような○×ゲーム(3目並べ)をVueでつくります。

ルール: プレイヤーが交互に○×をマスに置いていき、どちらかのマークが3つ並べば勝ちです。

また、実装内容は次のとおりです。

  • ○は赤文字、×は青文字にする。
  • どちらの番かが分かるように「○(×)プレイヤーさん、マスを選んでください」と表示する(つまり2Pプレイ)
  • ゲームが終了したら「○(×)さんの勝ちです。おめでとうございます!」と表示する
  • 引き分けの場合は「引き分けです!」と表示する

では実際にコードを書いていきましょう。

Vueの基本形をつくる

まずはVueが使えるように基本形をつくります。

<html>
<head>
    <style></style>
</head>
<body>
    <div id="app">

        <!-- ここがVueエリア -->

    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.5.17/dist/vue.min.js"></script>
    <script>

        new Vue({
            el: '#app'
        })

    </script>
</body>
</html>

やっていることは、cdnからVue本体を読み込み、appというidVueに設定しているだけです。

レイアウトをつくる

続いて×を書いていくゲームのマス目を作ります。

<html>
<head>

    <style>

        #table {

            margin:0 auto;
            border-collapse: collapse;
            border: 3px solid #ccc;

        }

        #table td {

            border:1px solid #ccc;
            height: 80px;
            width: 80px;
            text-align: center;
            vertical-align: middle;
            font-size: 75px;
            cursor: pointer;

        }

    </style>

</head>
<body>
    <div id="app">
        <table id="table">
            <tr>
                <td></td>
                <td></td>
                <td></td>
            </tr>
            <tr>
                <td></td>
                <td></td>
                <td></td>
            </tr>
            <tr>
                <td></td>
                <td></td>
                <td></td>
            </tr>
        </table>
    </div>
    <!-- 省略 -->
</body>
</html>

中身は3×3のマスがある<table>〜</table>タグを作っているだけです。
これを実行すると次のようになります。

では、このマス目を使ってVue(JavaScript)部分を作っていきましょう。

JavaScript部分をつくる

Vueに変数をつくる

まず、○×ゲームで必要な変数をつくっていきます。
必要なものは次の2点です。

  • マス目の状態(3×3の配列)
  • 選択できるプレイヤーのID

3×3の配列の中身は次のとおりです。

  • -1 ・・・ まだ選択されてない
  • 1 ・・・ ○が選択されている
  • 2 ・・・ ×が選択されている

また、プレイヤーIDは

  • 1 ・・・ ○のプレイヤー
  • 2 ・・・ ×のプレイヤー

そして、これらをVueの変数にするとこうなります。

new Vue({
    el: '#app',
    data: {
        states: [
            [-1, -1, -1],
            [-1, -1, -1],
            [-1, -1, -1]
        ],
        playerId: 1 // ○のプレイヤーから開始
    }
})

変数をバインディングする

では、設定した変数をHTML内にバインディングしていきます。
まずはマス目部分です。

<table id="table">
    <tr v-for="row in states">
        <td v-for="state in row">
            <div style="color:#f00;" v-if="state==1">○</div>
            <div style="color:#00f;" v-if="state==2">×</div>
        </td>
    </tr>
</table>

ここでやっていることは、先ほどつくったdata変数のstatesv-forを使ってループさせ、3×3のマスを作ると同時に、マス目の状態によって×、空白を切り替えています。

では、初期値を次のようにして実際に表示してみましょう。

states: [
    [2, 2, 2],
    [-1, -1, -1],
    [1, 1, 1]
]

うまくいきました。

では続いてplayerIdですが、これは現在マスを選ぶことができるプレイヤーのIDなので、このIDを使って次の2つの表示を切り替えるようにしてみます。

  • ○ プレイヤーさん、マスを選んでください
  • × プレイヤーさん、マスを選んでください
<table id="table">
    <!-- 省略 -->
</table>

<div style="text-align:center;">
    <div style="color:#f00;" v-if="playerId==1">「○ プレイヤーさん、マスを選んでください」</div>
    <div style="color:#00f;" v-if="playerId==2">「× プレイヤーさん、マスを選んでください」</div>
</div>

実行するとこうなります。

マス目が選択されたときのコードをつくる

では、実際にマス目が選択されたときのコードをつくっていきます。

まずはマス目になる<td>タグにクリックイベントでonSelect()が実行されるようにします。

<table id="table">
    <tr v-for="(row,rowsIndex) in states">
        <td v-for="(state,colsIndex) in row" @click="onSelect(rowsIndex, colsIndex)">
            <!-- 省略 -->
        </td>
    </tr>
</table>

なお、選択されたマスがどこなのかが分かるように、v-forにはそれぞれrowsIndexcolsIndexを追加してonSelect()へ送っています。

ではメソッド本体です。

new Vue({
    // 省略
    methods: {
        onSelect: function(rowsIndex, colsIndex) {

            if(this.states[rowsIndex][colsIndex] != -1) {

                alert('そのマスは、すでに選択されています!');

            } else {

                var states = JSON.parse(JSON.stringify(this.states))
                states[rowsIndex][colsIndex] = this.playerId;
                this.states = states;
                this.playerId = (this.playerId == 1) ? 2 : 1;

            }

        }
    }
})

この中で、まず選択されたマスの状態を確認し、もし-1でなければ(つまり、すでに選択済みなら)エラーを表示するようにしています。

そして、ここが重要なのですが、Vueの配列を更新するためには変数自体を書き換える必要があります。つまり、この場合だと

this.states[rowsIndex][colsIndex] = this.playerId;

としても、バインディングされた部分は新しく書き換えられることはありません。

そのため、少しまわりくどいですが、まず元々のデータを一旦JSONにして再度元に戻すことで配列をコピー。そして、選択されたマス目のデータを更新し、最後にthis.states全体を書き換えるという方法をとっています。(お好みでVue.set()を使ってもいいでしょう)

また、選択が済んだあとは、プレイヤーを交代させるためにplayerIdを変更するようにしています。

ゲームが完了したかどうかをチェックする

これでマス目の選択部分は完了しましたが、肝心のゲームに勝ったかどうかをチェックする機能がついていませんので、この部分を作っていきます。

onSelect: function(rowsIndex, colsIndex) {

    if(this.states[rowsIndex][colsIndex] != -1) {

        // 省略

    } else {

        // 省略

        var winnerId = this.getWinnerId();

        if(winnerId != -1) {

            this.states = [
                [-1, -1, -1],
                [-1, -1, -1],
                [-1, -1, -1]
            ];
            playerIds = {
                1: '○',
                2: '×'
            };
            alert(playerIds[winnerId] +' さんの勝ちです。おめでとうございます!');

        }

    }

},
getWinnerId: function() {

    for(var i = 0; i < 3 ; i++){

        // 横の列
        var row = this.states[i];
        if(this.isStatesFilled(row)) { return row[0];}

        // 縦の列
        var col = [
            this.states[0][i],
            this.states[1][i],
            this.states[2][i]
        ];
        if(this.isStatesFilled(col)) { return this.states[0][i]; }

    }

    // ななめ
    var skew1 = [
        this.states[0][0],
        this.states[1][1],
        this.states[2][2]
    ];
    if(this.isStatesFilled(skew1)) { return this.states[0][0]; }

    var skew2 = [
        this.states[0][2],
        this.states[1][1],
        this.states[2][0]
    ];
    if(this.isStatesFilled(skew2)) { return this.states[0][2]; }

    return -1;

},
isStatesFilled: function(states) {

    return(
        states[0] != -1 &&
        states[0] == states[1] &&
        states[1] == states[2]
    );

}

少しコードが長いのでひとつずつ紹介していきます。

まずgetWinnerId()の中では、statesの中身をisStatesFilled()を使って次の勝利条件をチェックし、さらに勝ったプレイヤーのIDを取得しています。

  • -1を含まず(まだ選択されていないため)
  • 全ての数字が同じ値である

そして、もしこの勝利条件を満たす場合は1もしくは2の勝ったプレイヤーIDを返し、アラートを表示。さらにstatesを初期値へ戻します。

引き分けの場合をチェックする

○×ゲームは、引き分けの場合もありますので、勝利条件を満たしていない場合はisDraw()で引き分けをチェックするようにします。

onSelect: function(rowsIndex, colsIndex) {

    if(this.states[rowsIndex][colsIndex] != -1) {

        // 省略

    } else {

        // 省略

        if(winnerId != -1) {

            // 省略

        } else if(this.isDraw()) {

            this.states = [
                [-1, -1, -1],
                [-1, -1, -1],
                [-1, -1, -1]
            ];
            alert('引き分けです!');

        }

    }

},

// 省略

isDraw: function() {

    for(var i in this.states) {

        var row = this.states[i];

        for(var j in row) {

            var state = row[j];

            if(state == -1) {

                return false;

            }

        }

    }

    return true;

}

isDraw()の中ではstatesの中身をすべてチェックして-1が存在していたら(つまりまだゲームは終わっていない)falseを返し、そうでなければゲームは終了しているのに勝敗が決まっていない(=引き分け)ということでtrueを返します。

そして、もし引き分けの場合はアラートを起動して完了です。

お疲れ様でした!

おまけ – 1(リファクタリング)

今回紹介したコードの中には複数回同じコードが出てくる場面があります。
そうです。statesを初期化する部分ですね。

this.states = [
    [-1, -1, -1],
    [-1, -1, -1],
    [-1, -1, -1]
];

同じコードはできるだけ一回書くだけにしておくほうがいいのでinit()というメソッドを作ってこの中でstatesを初期化するようにリファクタリングしてみましょう。

methods: {
    init: function() {

        this.states = [
            [-1, -1, -1],
            [-1, -1, -1],
            [-1, -1, -1]
        ];

    },

    // 省略

そして、init()を呼び出す場所は先ほどのonSelect()の中ですね。

onSelect: function(rowsIndex, colsIndex) {

    if(this.states[rowsIndex][colsIndex] != -1) {

        // 省略

    } else {

        // 省略

        if(winnerId != -1) {

            this.init();
            playerIds = {
                1: '○',
                2: '×'
            };
            alert(playerIds[winnerId] +' さんの勝ちです。おめでとうございます!');

        } else if(this.isDraw()) {

            this.init();
            alert('引き分けです!');

        }

    }

},

そして、もう一つあります。
データの初期値です。

今まで直接値を設定していましたが、空白の配列へ変更します。

data: {
    states: [], // 初期値はmountedの中で設定
    // 省略
},

そして、ページが表示されたらすぐに実行されるmountedの中でinit()を実行すると可読性もあがりますし、コードの共通化ができて保守もしやすいでしょう。

mounted: function() {

    this.init();

}

おまけ – 2(勝った時に画像を表示する)

先ほどのコードでは、○×どちらかのプレイヤーが勝ったときにアラートを表示するだけでしたが、ちょっとこれだけでは味気ないので勝ったら画像を表示するようにしてみます。(ただし、対戦中は表示しません)

まずは変数部分です。
winnerTextという変数を追加し、どちらかが勝ったときのメッセージをここへ格納します。

data: {

    // 省略

    winnerText: ''
},

そして、レイアウト部分です。

<div v-if="winnerText" style="text-align:center;">
    <h2 v-text="winnerText"></h2>
    <img src="/images/congrats.png">
</div>

もしwinnerTextの中にメッセージが入っていれば、そのテキストと画像を表示するようにします。

そして、winnerTextはマスが選択される度に初期化しますが、もしどちらかが勝った場合だけテキストを格納します。(こうすることで対戦中にテキストと画像が表示されることはなくなります)

onSelect: function(rowsIndex, colsIndex) {

    this.winnerText = '';

    // 省略

        if(winnerId != -1) {

            // 省略

            this.winnerText = playerIds[winnerId] +' さんの勝ちです。おめでとうございます!';

        }

    // 省略

}

デモを用意しました

今回のコードを実際に体験していただくためにデモページを用意しました。
ぜひ楽しんで体験してみてくださいね。

教材ソースコードをダウンロードする

今回実際に開発したコードを以下からダウンロードすることができます。cdnを使っているので、展開してすぐ体験できますよ!

※ ただし画像をご自身で用意してください。

Vueで○×ゲームをつくる

おわりに

ということで今回は、Vueを使って子供の頃よくやったゲーム「○×ゲーム」を作ってみました。

できるだけ分かりやすくすることを心がけてコードを書きましたが、やはり冒頭でもいったとおり、ゲームのコードは色々と数学的な発想も必要になってくるので見た目よりは少し複雑なコードになってしまったかもしれません。

ただ、以前アプリで個人的にゲームを作ったことがあるのですが、やっぱりゲームを作るのは楽しいですね。

そういう意味では、子供の頃テレビゲームを夢中になってやった経験も生きてきているのかもしれません(・・・と信じたいです ^^;)

ではでは〜!

この記事が役立ちましたらシェアお願いします😊✨