Vueで簡単なリバーシ・ゲームを作ってみる(DL可)

こんにちは❗フリーランス・コンサルタント&エンジニアの 九保すこひ です。

さてさて、ここのところVueに関する記事をお届けしていますが、内容としては(もちろんですが)ウェブページの構築のためのテクニックでした。

ただ、JavaScriptでもPHPでもそうですが、使い方によってはウェブサイトのためだけではない使い方ができたりします。

例えば、ゲームです。

あまり仕事として本格的にブラウザでゲームをつくることは少ないかもしれませんが、個人的にゲームをつくることはとてもプログラミングの勉強になると思っています。

なぜなら、「実際に自分で使ってみて楽しい」があるからです。

そこで!

今回はVue.jsの学習のためになる(であろう)とても簡単なリバーシ・ゲームをつくる方法をご紹介したいと思います。

ぜひVue学習のお役に立てると嬉しいです。

開発環境: Vue 2.6

やりたいこと

おそらくリバーシを知らない方はあまりいないと思いますので、ゲームの詳しい説明は割愛しますが、以下のようなものですね。

これをHTMLとJavaScript(Vue)だけを使って実装します。
そして、今回は学習用のコードということで要件は以下の4つです。

  • 交互に黒白の石を置いていく
  • 同じ色の石で挟んだらひっくり返る
  • 全てのマスに石が置かれたら、どっちの色が勝ったかを表示する
  • 石は空白の場所だけしか置けない

※ なお、コードが増えすぎてしまうので、今回石はどこでも置けるようになっています。

では実際にやってみましょう!

Vueで黒・白の石を表示するコンポーネントをつくる

まず盤上の石はたくさん表示することになりますから、石を表示するためだけのコンポーネントを先につくっておきましょう。

実際のコードは次のとおりです。

var stoneComponent = {
    props: ['type'],
    template: '<span v-html="content"></span>',
    computed: {
        content() {

            if(this.type == '-1') { // 黒

                return '&#x26AB;';

            } else if(this.type == '1') { // 白

                return '&#x26AA;';

            }

            return '&nbsp;';

        }
    }
};

内容としては、typeの中身によって黒/白/空白の石を表示だけのシンプルなものです。

では、このコンポーネントをVueのインタンスに登録しておきましょう。

<script>

    // 石のコンポーネント
    // (省略。さっきのコード)

    new Vue({
        el: '#app',
        components: {
            stone: stoneComponent
        },
    });

そして、使い方は次のとおりです。

<!-- 黒い石 -->
<stone type="-1"></stone>

<!-- 白い石 -->
<stone type="1"></stone>

<!-- 空白(何も表示しない) -->
<stone type="0"></stone>

※ ちなみに&#x26AA;&#x26AB;は絵文字で、表示するブラウザに若干表示が違います。なお、絵文字に興味がある方は、Let’s Emojiさんで検索できます。

リバーシの盤をつくる

続いては、石を置いていく盤です。せっかくプログラムでつくるので、マス目の数をsizeという変数にいれておき、この数字を変更すると自動的に盤が拡大・縮小するようにしてみましょう。

また、盤上の石データstonesの中身は以下の内容で実装します。

  • -1 ・・・ 黒い石
  •  1 ・・・ 白い石
  •  0 ・・・ 空白

(例:5x5マスで全て黒い石の場合)

[
    [-1, -1, -1, -1, -1],
    [-1, -1, -1, -1, -1],
    [-1, -1, -1, -1, -1],
    [-1, -1, -1, -1, -1],
    [-1, -1, -1, -1, -1],
]

では、実際のコードです。

<html>
<head>
    <style>

        table {

            border-collapse: collapse;

        }

        td {

            border: 1px solid #999;
            background: #f0f0f0;
            width: 30px;
            height: 30px;
            text-align: center;
            vertical-align: middle;

        }

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

    <!-- リバーシの盤 -->
    <table>
        <tr v-for="y in size">
            <td v-for="x in size">
                <stone :type="getStone(x, y)"></stone>
            </td>
        </tr>
    </table>

</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.0"></script>
<script>

    // 石のコンポーネント
    // (省略)

    new Vue({
        el: '#app',
        components: {
            stone: stoneComponent
        },
        data: {
            size: 8,
            stones: []
        },
        methods: {
            initStone() {

                var stones = [];
                var size = this.size;

                for(var x = 0 ; x < size ; x++) {

                    var stoneLine = [];

                    for(var y = 0 ; y < size ; y++) {

                        stoneLine.push(0);  // 0は、空白

                    }

                    stones.push(stoneLine);

                }

                this.stones = stones;

            },
            getStone(x, y) {

                return this.stones[x-1][y-1];

            }
        },
        created() {

            this.initStone();

        }
    });

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

まずこの中で一番重要なのは、initStone()で石データを初期化している部分です。このコードの中ではsizeで指定した数の2乗分(例えば5 x 5)の石データstonesが作成されることになり、このデータを元にして<table></table>内のマスを表示していくことになります。(つまり、sizeを変えると盤の大きさも自動で変わります)

また、initStone()created()で呼び出されていることにも注意してください。通常はmounted()を使ってページが読み込まれた時点で呼び出しますが、stonesはそれよりも少し早く準備ができていないとエラーが発生してしまうからです。(描画が先に必要になるため)

そして、このコードを実行するとこのようになります。(ただし、分かりやすくするため全て黒い石をセットしています)

盤上に石を置けるようにする

では、ここから盤上のマス目をクリックして「石を交互に置いていく」という部分をつくっていきます。

まずは実際のコードです。

<html>
<head>
    <style>

        // 省略

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

    <!-- リバーシの盤 -->
    <table>
        <tr v-for="y in size">
            <td v-for="x in size" @click="onStoneSet(x, y)">
                <stone :type="getStone(x, y)"></stone>
            </td>
        </tr>
    </table>

</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.0"></script>
<script>

    // 石のコンポーネント
    // 省略

    new Vue({
        el: '#app',
        components: {
            stone: stoneComponent
        },
        data: {
            size: 8,
            stones: [],
            currentStoneId: -1,    // -1 => 黒い石、1 => 白い石,
        },
        methods: {
            initStone() {

                // 省略

            },
            getStone(x, y) {

                // 省略

            },
            copyStone() {

                return JSON.parse(JSON.stringify(this.stones));

            },
            onStoneSet(x, y) {

                if(this.stones[x-1][y-1] == 0) {    // 選んだ場所が空白かどうか?

                    var newStones = this.copyStone();
                    newStones[x-1][y-1] = this.currentStoneId;
                    this.stones = newStones;
                    this.currentStoneId *= -1;  // 白黒交代

                } else {

                    alert('すでに石が置かれています');

                }

            }
        },
        created() {

            this.initStone();

        }
    });

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

この中でメインで追加したのは、onStoneSet()メソッドです。

盤上のマス目をクリックすると、実行されるように<td>タグの中に

@click="onStoneSet(x, y)"

として登録しています。

そして、このメソッドの中ではまず「その場所が空白か?」をチェックし、もしすでに石が置かれていたらエラーメッセージを表示し、置かれていないようだったらstonesの中身を更新しています。

ちなみに、なぜわざわざcopyStone()というメソッドを作って一旦石データをコピーしてからstonesへ代入しているのかというと、Vueは配列やオブジェクト内の変更はリアルタイムに更新できないからです。(ちなみにwatchではdeepを設定することで対応が可能です。)

ということで、マス目をクリックするとこのように石が追加されるようになりました。

石をひっくり返す部分をつくる

では、ここからが今回の開発で一番複雑な部分「石をひっくり返す」部分になります。そのため、コードより先にどのように実装するかをご紹介します。

例えば今、5x5の盤の真ん中(x,y) = (3,3)の位置に黒い石が置かれたとします。この場合、

  • 左上
  • 右上
  • 右下
  • 左下

の8方向に移動しながら隣のマスには何があるかをチェックしていくことになります。

例えば、この例(3,3に置いた状態)で上方向なら、

  • 座標(3,2)
  • 座標(3,1)
  • 座標(3,0)→ 盤の外に出た

というように1つずつ移動して、移動先のマス目の状態によって次のように処理をします。

  • 相手の石だった ・・・ ひっくり返せる可能性があるので座標を一時的に保存
  • 自分の石だった ・・・ 同じ色で挟んだので、上記で保存した座標を自分の石にする(ひっくり返す)
  • 空白だった ・・・ 何もしない
  • 盤上の外に出た ・・・ 何もしない

そして、これを8方向で実行するとリバーシの「ひっくり返す」機能を実装することができます。

では、実際のコードです。

<html>
<head>
    <style>

        // 省略

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

    <!-- リバーシの盤 -->
    <!-- 省略 -->

</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.0"></script>
<script>

    // 石のコンポーネント
    // 省略

    new Vue({

        // 省略

        methods: {

            // 省略

            changeStone(x, y) { // 同じ色で挟まれた場合にひっくり返す

                var movingCollection = [ // 移動する方向
                    {x: -1, y: -1}, // 左上
                    {x: 0, y: -1}, // 上
                    {x: 1, y: -1}, // 右上
                    {x: 1, y: 0}, // 右
                    {x: 1, y: 1}, // 右下
                    {x: 0, y: 1}, // 下
                    {x: -1, y: 1}, // 左下
                    {x: -1, y: 0}, // 左
                ];
                var baseX = x - 1; // 置かれた石の座標:X
                var baseY = y - 1; // 置かれた石の座標:Y
                var changingStoneId = this.currentStoneId * -1; // ひっくり返す石

                for(var i = 0 ; i < movingCollection.length ; i++) {

                    var moving = movingCollection[i];
                    var checkingX = baseX;
                    var checkingY = baseY;
                    var changingPositions = []; // ひっくり返す(かもしれない)座標

                    innerLoop:
                        for(var j = 0 ; j < this.size ; j++) {

                            checkingX += moving.x; // チェックする場所を移動
                            checkingY += moving.y; // チェックする場所を移動

                            if(checkingX < 0 ||
                                checkingY < 0 ||
                                checkingX >= this.size ||
                                checkingY >= this.size) { // 盤上を出た

                                break innerLoop;

                            }

                            var checkingStoneId = this.stones[checkingX][checkingY];

                            if(checkingStoneId == this.currentStoneId) { // 自分の色だった場合

                                var newStones = this.copyStone();

                                for(var k = 0 ; k < changingPositions.length ; k++) {

                                    var changingPosition = changingPositions[k];
                                    var changingX = changingPosition.x;
                                    var changingY = changingPosition.y;
                                    newStones[changingX][changingY] = this.currentStoneId;

                                }

                                this.stones = newStones;
                                break innerLoop;

                            } else if(checkingStoneId == changingStoneId) { // 相手の色だった場合

                                changingPositions.push({x: checkingX, y: checkingY});

                            } else { // 空白の場合

                                break innerLoop;

                            }

                        }

                }

            },

            // 省略

        },
        created() {

            this.initStone();

        }
    });

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

このコードの中で重要なのは、innerLoop:の部分です。

実は私もあまり使ったことが無いのですが、これは例えば「2重になったループの内側を抜けたい」という場合に使う方法です。

そのためコード内ではbreak innerLoop;として、内側のループだけを抜けています。

どちらが勝ったかを表示する機能をつくる

では最後に、マス目がすべて埋まったときに石の数を数え「どちらが勝ったか?」を表示してみましょう。

また、せっかくですので「今は白黒どちらの番か?」も表示する機能もつけてみます。

<html>
<head>
    <style>

        // 省略

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

    <!-- 省略 -->
    
    <span style="background:#ccc;padding:7px 10px;">
         <stone :type="currentStoneId"></stone> 石を置いてください
    </span>
    <br>
    <br>
    <div v-if="message" v-html="message"></div>

</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.0"></script>
<script>

    // 石のコンポーネント
    // 省略

    new Vue({

        // 省略

        data: {

            // 省略

            message: ''
        },
        methods: {

            // 省略

            stoneCounts() {

                var counts = {
                    black: 0,
                    white: 0,
                    total: 0
                };
                var size = this.size;

                for(var x = 0 ; x < size ; x++) {

                    for(var y = 0 ; y < size ; y++) {

                        var stoneId = this.stones[x][y];

                        if(stoneId == -1 || stoneId == 1) {

                            if(stoneId == -1) {

                                counts.black++;

                            } else if(stoneId == 1) {

                                counts.white++;

                            }

                            counts.total++;

                        }

                    }

                }

                return counts;

            },
            onStoneSet(x, y) {

                if(this.stones[x-1][y-1] == 0) {    // 選んだ場所が空白かどうか?

                    // 省略

                    var fullCount = this.size * this.size;
                    var stoneCounts = this.stoneCounts();

                    if(stoneCounts.total == fullCount) {

                        if(stoneCounts.black > stoneCounts.white) {

                            this.message = '黒の勝ち!';

                        } else if(stoneCounts.black < stoneCounts.white) {

                            this.message = '白の勝ち!';

                        } else {

                            this.message = '引き分け!';

                        }

                    }

                } else {

                    alert('すでに石が置かれています');

                }

            }
        },
        created() {

            this.initStone();

        }
    });

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

重要なのはonStoneSet()内で呼ばれているstoneCounts()です。

このメソッドは石が置かれるごとに実行されますが、この中では白と黒の石の数だけでなく白黒合わせたトータルの石の数も計算するようにし、その数がマス目と同じ数になったらゲームが終了したものとしてどちらが勝ったかを表示するようにしています。

また、以下は今どちらが石を置く番かを表示する部分ですが石のコンポーネントを作っているので、typeをリアルタイムに変更できるようにするだけで実装ができます。

<span>
    <stone :type="currentStoneId"></stone> 石を置いてください
</span>

これを実行したものがこちらです。

テストしてみる

では、今回のコードを実際にテストしてみましょう!

はい!
うまくいったようです😊✨

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

今回実際に作成したソースコード一式を以下からダウンロードすることができます。

VUEもCDNで呼び出しているので展開したらすぐ試すことができますよ!

Vueで簡単なリバーシ・ゲーム

おわりに

今回は複雑になりすぎないように「石が置ける or 置けない」という機能をつけていませんが、今回のコードを拡張すればそれほど難しいことではないと思います。また、最初に盤上には白黒2つずつ配置してもいいかもしれませんね。

ちなみに、私の知っているリバーシ(世代的にはオセロと呼んでます)のルールって次のもので正しいんですかね?

  • 石を置く時は必ずひっくり返さないといけない
  • ひっくり返す場所が無い時はスキップ

もしかすると、トランプみたいに「田舎ルール」と呼ばれる亜流バージョンの可能性もありますので、今回はこの部分を作らなくてちょうど良かったかもしれませんね😂

ということで、みなさんもぜひVueの使い方をゲームをつくることで体験してみてはいかがでしょうか。

ではでは〜!

開発のご依頼お待ちしております 😊✨
開発のご依頼はこちらから: お問い合わせ
どうぞよろしくお願いいたします!
このエントリーをはてなブックマークに追加       follow us in feedly  

開発効率を上げるための機材・まとめ