JavaScript
jQuery
d3.js

「人間はJavaScriptにいずれ負ける」ことを実感するツールを作った

はじめに

この記事は、JavaScript Advent Calendar 2018の18日目の記事です。

クソアプリ2 Advent Calendar 2018でも同じような内容の記事を書いています
(こちらは、よりクソな内容の記事となっています)

クソアプリ2 Advent Calendar 2018

ゲームを作りました

これはJavaScriptの速さを身をもって体験ができるアプリです。
アプリと言うかゲームですね。

JavaScriptよりも早く、ターゲットに到達するのを競うものです。

hvsj.gif

上の画像でなんとなく雰囲気わかる方もいるかも知れませんが、
緑の円よりも早く、もう一つの円に到達する(マウスオーバーする)ことを繰り返します。

プレイする

技術的な話

実装には、D3.jsともはや化石と化しそうなjQueryを使用しています。
(といっても、d3の要素はほぼないです)

このゲームは主に以下のクラスで動いています。

  • Targetクラス・・・みんなが追いかける丸
  • Trackerクラス・・・我々と戦う緑の丸
  • Navigatorクラス・・・今の現状を教えてくれるヤツ

GitHub - ソースコード

Targetクラスはただ動くだけ

みんなが追いかけるTargetクラスはただただ動けといわれれば、ランダムにうごくだけのクラスです。

    /**
     * Target クラス
     *
     * Navigatorクラスが操作をする。動かない丸。
     * Trackerクラスがこいつを追い続ける。
     *
     * @constructor
     */
    Target = function (svg, limitX, limitY) {
        // d3のプリセットカラーの中から適当に色を選ぶ
        var colors = d3.scale.category10().range();
        var color = colors[parseInt(Math.random() * colors.length)];

        // 活動限界を決める
        this.limitX = limitX;
        this.limitY = limitY;
        // d3のメソッドを使ってSVGを生成する
        this.circle = svg.append('circle')
            .attr('r', 10)
            .attr('cx', Util.randomX(limitX))
            .attr('cy', Util.randomY(limitY))
            .attr('fill', color)
            .attr('id', 'target');
    };
    // ランダムな場所に瞬間移動させる
    Target.prototype.move = function () {
        this.circle
            .attr('cx', Util.randomX(this.limitX))
            .attr('cy', Util.randomY(this.limitY));
    };
    // 座標を返す
    Target.prototype.getPoint = function () {
        return {
            x: this.circle.attr('cx'),
            y: this.circle.attr('cy')
        }
    }

Trackerクラス(緑の丸)は常に探しながら動く

じつは、この緑の丸は「ターゲットの位置が何処にあるかわかっていません」
人間と戦うに当たり、イーブンな土台に上げるためにあえてそうしています。

まず、相手との距離をスコア化します。
(近いと減る、遠いと増える、みたいな)

Trackerクラス全16方向(東西南北とその間、さらに北北西などの方位も)に試しに動いてみて、
Navigatorクラスに対して「どの方向に動いたやつが一番スコアが良かった?」と聞いて、それで一番スコアの良い(相手との距離が一番縮まる)方向に動きます。

それを定期的に続けることで
「何処にいるかは知らないけど、こっちにいったら近づいてる」
という情報のフィードバックをずっと得ながら移動していきます。

イメージで言うとドラゴンボールに出てくる「ドラゴンレーダー」のような動きでしょうか

仕組み図解

    /**
     * Trackerクラス
     *
     * ターゲットを追い続ける追跡者
     *
     * @param svg
     * @param navigator
     * @param limitX
     * @param limitY
     * @constructor
     */
    Tracker = function (svg, navigator, limitX, limitY) {

        // Targetより小さめの緑の円を生成
        this.circle = svg.append('circle')
            .attr('r', 5)
            .attr('cx', Util.randomX(limitX))
            .attr('cy', Util.randomY(limitY))
            .attr('fill', 'green')
            .attr('id', 'tracker');

        // 〜(略)〜

        // 移動する方向をxとyの成分で保持する
        this.forwardMaster = {
            // 上段 (北西・北・北東)
            7: {x: -1, y: -1},
            8: {x: 0, y: -1},
            9: {x: 1, y: -1},

            // 中段 (西・ニュートラル・東)
            4: {x: -1, y: 0},
            5: {x: 0, y: 0},
            6: {x: 1, y: 0},

            // 下段 (南西・南・南東)
            1: {x: -1, y: 1},
            2: {x: 0, y: 1},
            3: {x: 1, y: 1},

            // 北北西と北北東
            10: {x: -1, y: -2},
            11: {x: 1, y: -2},

            // 西北西と東北東
            12: {x: -2, y: -1},
            13: {x: 2, y: -1},

            // 西南西と東南東
            14: {x: -2, y: 1},
            15: {x: 2, y: 1},

            // 南南西と南南東
            16: {x: -1, y: 2},
            17: {x: 1, y: 2}

        };

        this.navigator = navigator;
    };


    /**
     * 判定
     *
     * @returns {number}
     */
    Tracker.prototype.judge = function () {

        var scoreArr = [];
        var maxIndex = 1;
        var minIndex = 1;

        // 各方向に向かって、一番近くなるスコアを集計する
        for (var i = 1; i <= this.maxForwardIndex; i++) {
            // 5は動かない(ニュートラル)なので飛ばす
            if (i != 5) {
                scoreArr[i] = this.mathScore(i);

                if (scoreArr[maxIndex] <= scoreArr[i]) {
                    maxIndex = i;
                }
                if (scoreArr[minIndex] >= scoreArr[i]) {
                    minIndex = i;
                }
            }
        }

        // ここに来た時点で、スコアが高いforward値が求められている
        return maxIndex;
    };


    /**
     * 移動スコアを計算する
     * @param forward
     * @returns {number}
     */
    Tracker.prototype.mathScore = function (forward) {
        var i;
        var count = 0;
        var sum = 0;

        while (this.history[forward].queue.length > this.step * 2) {
            this.history[forward].queue.shift();
        }

        // その方向の要素を全て洗う
        for (i in this.history[forward].queue) {
            sum += this.history[forward].queue[i].line;
            count++;
        }

        // 最後の要素だけをもう一回
        sum += this.history[forward].queue[i].line;
        count++;

        // 指数移動平均スコアを求める
        return (sum / count);
    };

実際には計算が早くなってるわけではない

ネタバレしてしまうと、setTimeoutの間隔がどんどん早くなっていっているだけです。

    Navigator.prototype.decrementDelay = function() {
        // 今の秒間隔(msec)の10分の1だけ間隔が短くなる
        // 最初の遅い時とだんだん早くなった時でも体感の変化率を揃えるため
        // (早いときは1msecの重みが違う)
        var substract = this.delay / 10;
        this.delay = this.delay - substract;
        return this.delay;
    }

そうです、「待ってもらっていただけ」です

筆者のスコア

作った人間はこれが限界でした。

スクリーンショット 2018-12-16 20.05.37.png

おわりに

本当は、強化学習機能を搭載して、
「このスコアの並びだったらだいたいこれぐらいの距離だな・・・」
という一気にジャンプする機能を付けたかったのですが、それはまたの機会に。

ていうか、Advent Calendarの記事がこんなのでいいのだろうか・・・