9
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

MTGのために画像と名前をさっとサジェストで検索する

Last updated at Posted at 2018-07-05

ニコニコ動画で、シチュエーション1に即したカード名が
《日本語/英語》
でコメントされる流れが好きです。

しかし日本名はともかく英語までいちいち調べるのは面倒2なので、情報をさっと取れないかなと調べました。
さすが「もっともよく遊ばれているTCG」
APIも充実していました。

あ、いまさらですがMTGはミーティングではなくMagic The Gatheringのことです。(お約束

完成品

mtg.gif

MtG Card Suggest

khsk/MtG-card-suggest: Search and Completion from your input

API

MTG Developers
を日本語で検索しています。

サジェスト

アニメの名言を簡単に引用できるChrome extension『Kotoha』作りました - Konifar's WIP
で使われている
At.js
を使用しています。

コード

ライブラリ側
const API_URL = 'https://api.magicthegathering.io/v1/cards'


const mtg_callback = {
    remoteFilter: (() => {
        let oldQuery
        let oldCards
        let promise = null

        return (query, callback) => {
            // 気休めの接続低減
            // 変換開始時の空文字queryを排除している
            if (!query) {
                return
            }
            // 気休めの接続低減
            // カーソル移動でイベントが発生するので同値なら結果を使い回す
            if (query === oldQuery) {
                callback(oldCards)
                return
            }

            if (promise) {
                // 気休めの負荷緩和
                // 英字入力やカーソル移動で通信が多発するので、古いのはabortしてやる
                promise.abort()
                promise = null;
            }

            promise = $.getJSON(
                API_URL,
                {
                    name: query,
                    language: 'Japanese',
                },
                (data) => {
                    let cards = $.map(data.cards, (card) => {
                        const foreignData = $.grep(card.foreignNames, (foreignName) => {
                            return (foreignName.language == 'Japanese')
                        })[0]

                        return {
                            name: card.name,
                            jname: foreignData.name || card.name,
                            jimageUrl: foreignData.imageUrl || card.imageUrl,
                        }
                    })

                    // 外部通信なので、検索中と区別がつくように結果0用のリストを追加する
                    if (cards.length == 0) {
                        cards = [{
                            name: 'Not matched',
                            jname: 'Not matched',
                            jimageUrl: 'Not matched',
                        }];
                        cards[0].atwho_order = 0

                        console.log('cards2', cards)
                    }

                    oldQuery = query
                    oldCards = cards
                    promise = null
                    callback(cards)
                }
            )
        }
    })(),


    sorter: (query, items, searchKey) => {
        var _results, i, item, len;
        if (!query) {
            return items;
        }
        _results = [];
        for (i = 0, len = items.length; i < len; i++) {
            item = items[i];
            // remoteFilterで作ったNot matchedを活かすため、手動でordwerを決めた場合は判定しないようカスタマイズ
            if (typeof item.atwho_order === 'undefined') {
                item.atwho_order = item.atwho_order || new String(item[searchKey]).toLowerCase().indexOf(query.toLowerCase());
            }
            if (item.atwho_order > -1) {
                _results.push(item);
            }
        }
        return _results.sort(function (a, b) {
            return a.atwho_order - b.atwho_order;
        });
    },
}
利用ページ側
      $('textarea, #nameInput').atwho({
          at: "\/mtg",
          displayTpl: "<li>${jname} / ${name}</li>",
          insertTpl: "《 ${jname} / ${name} 》",
          limit: 200,
          callbacks: mtg_callback,
          searchKey: 'jname', // ここnullで動かしたいのだが
      })
      $('#imageInput').atwho({
          at: "\/imtg",
          displayTpl: "<li>${jname} / ${name}</li>",
          insertTpl: "${jimageUrl}",
          searchKey: 'jname', // ここnullで動かしたいのだが
          callbacks: mtg_callback,
      })
      $('#imageInput').change(function() {
        $('#preview').attr('src', $('#imageInput').prop('value'))
      })

At.jsの改変とコールバック実装

IMEのバグ修正?の取り消し

Return early for undefined events by markedmondson · Pull Request #534 · ichord/At.js
の変更を意図的に抜き、フィルター側で空文字のqueryを弾いて居ます。 これは検索開始時の空文字query検索を防ぐとともに、変換確定時にqueryが実行されるようにする処置です。
(逆に言えばこのプルリクが適用されていると、変換確定したときに検索されないです)

const mtg_callback = {
    remoteFilter: (() => {
        let oldQuery
        let oldCards
        let promise = null

        return (query, callback) => {
            // 気休めの接続低減
            // 変換開始時の空文字queryを排除している
            if (!query) {
                return
            }
}

ここのifでプルリクの代替としています。

検索回数低減

先の空文字検索を防ぐガードに加え、もう少しガードを追加しています。

            // 気休めの接続低減
            // カーソル移動でイベントが発生するので同値なら結果を使い回す
            if (query === oldQuery) {
                callback(oldCards)
                return
            }

            if (promise) {
                // 気休めの負荷緩和
                // 英字入力やカーソル移動で通信が多発するので、古いのはabortしてやる
                promise.abort()
                promise = null;
}

コメントそのままですが、検索の発火がキャレットの移動でも走る繊細さなので、結果を一つだけキャッシュしたり、ローマ字検索などで先にしてしまっている検索をabortして最後の完成queryだけで検索しようと画策しています。
効果のほどは知らん…

APIの制限は一時間に5000回と太っ腹ですが、触っていて気になるレベルでしたのでいろいろ試しました。

検索結果なしの表示

普通?は内部の候補を表示するので高速だと思いますが、今回は外部との通信なので少しばかり待ち時間が生じます。
そしてAt.jsは候補が0のときに何も出さない作りなので、
「検索待ちなのか検索結果が0なのか」
がわからない問題がありました。
そこで、

                   // 外部通信なので、検索中と区別がつくように結果0用のリストを追加する
                    if (cards.length == 0) {
                        cards = [{
                            name: 'Not matched',
                            jname: 'Not matched',
                            jimageUrl: 'Not matched',
                        }];
                        cards[0].atwho_order = 0

                        console.log('cards2', cards)
}

と検索結果が0のときにダミーの候補を作ってやるようにしました。
atwho_orderが検索キーワードとindexOfして出来たint値で、!-1なら一致したということです。

ただ、検索結果側のみで手動でatwho_orderを偽装するだけではうまく動きませんでした。

というのも、atwho_orderが作られるのはsorterのタイミングだったからでした。

At.js/default.coffee at master · ichord/At.js
At.js/jquery.atwho.js at 1b7a52011ec2571f73385d0c0d81a61003142050 · ichord/At.js

なので、デフォルトのsorterから手動で作ったatwho_orderを見逃す実装を追加しました。

    sorter: (query, items, searchKey) => {
        var _results, i, item, len;
        if (!query) {
            return items;
        }
        _results = [];
        for (i = 0, len = items.length; i < len; i++) {
            item = items[i];
            // remoteFilterで作ったNot matchedを活かすため、手動でordwerを決めた場合は判定しないようカスタマイズ
            if (typeof item.atwho_order === 'undefined') {
                item.atwho_order = item.atwho_order || new String(item[searchKey]).toLowerCase().indexOf(query.toLowerCase());
            }
            if (item.atwho_order > -1) {
                _results.push(item);
            }
        }
        return _results.sort(function (a, b) {
            return a.atwho_order - b.atwho_order;
        });
},

問題点

ダブる

image.png
(候補すべて取り消し)

APIは再録ごとに別カードとして扱うので、同じ候補がたくさんでます。
テキストやイラスト違いなどがあるので一概に悪いわけではないのですが…

オブジェクトのdistinctは
JavaScriptのオブジェクト配列に対してdistinct的な事(重複を排除した結果を返す)をする - Qiita
によると
O(n^2)らしいので、うーん…

日本語名がないカードがある

たとえば、有名なBlack Lotusは日本語版未発売?と考えられますが、
勝舞くんが使っていた筋肉スリヴァー/Muscle Sliver
などテンペストブロック周りの古いカードなどにも日本語名がありません。
https://api.magicthegathering.io/v1/cards?name=Muscle
画像も無い場合があります。
新しめのカードは最初から充実しているのですが。
なので、ダブっている場合でもリストの下の方が取れる可能性が高いです。

あと効果やフレーバーまで全言語対応してくれると…は高望みですね。

最大の問題点

画像URLを取得しようとすると、手が勝手にimgtと打ってしまうこと…

あとアドオンにして配布しないと特に便利ではないことですねー…。


image.png

それ(MTGじゃなくて)HSだよ

他の

At.jsを使うとき、依存物のCaret.jsを誤って
caret - cdnjs.com - The best FOSS CDN for web related libraries to speed up your websites!
accursoft/caret
にしていてハマりました。
ちゃんと
ichord/Caret.js
使おうね。

  1. カードゲームにまったく関係ない動画の

  2. Wikiに併記してある

9
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?