stat.ink

stat.ink internals: 匿名化された名前って何だ


stat.ink のバトル詳細を見ると、投稿者以外のデータがこのようによくわからない名称で置き換えられていることがあります。(設定やログイン状態によって変わります)

この名前、どのような特徴があるかご存じですか?

次の画像は左半分が上の画像と同じ、右半分はその次のバトルです。

赤い線で結んだ7つ(匿名化されているのは6つ)の「名前」が一致しているのがわかります。
お気づきかと思いますが、これは「同じ人」です。

stat.ink の「匿名化された名前」は、次のような特徴があります。

  • 同じアカウントは同じ名前になる
  • 違うアカウントは(ほとんど)同じ名前にならない
  • 日付が変わってもずっと同じ
  • プレーヤー名が変わっても同じ
  • ブキやギアが変わっても同じ(上の画像を見ると、実際に何人かブキが変わっています)

色々並べた結果、却ってわかりづらくなった気がしますが、「同じイカなら同じ名前」になる、そんな特徴があります。
これが何に使えるかというと、「名前は隠れているのに、同じイカであることがわかる」ので、フレンドがレビューに使えます。(イカリング見ればいいといえばそうですが)
あまり好ましい使い方ではないですが「このイカがいるほうが負けてるよね」の検証にも使えます。

ちなみに、その横のアイコンも同じような仕組みです。

実はここまでは前振りで、イカ、技術的な話になります。

この匿名化の実装ですが、SquidTracks の詳細表示を元にしています。
が、実は大きく異なるところがあります。

SquidTracks はページを切り替える度にランダム(のように見える)に名前が変わりますが、stat.ink では上述の通り固定されています。

実装としてはどちらも sillyname という JavaScript のライブラリを利用しています。
とりあえず、sillyname を使ってみましょう。

sillyname は JavaScript ライブラリで、NPM でのインストールに対応しているので、とりあえずこんな package.json をつくってみます。

{
  "name": "sillytest",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "license": "Unlicense",
  "dependencies": {
    "sillyname": "^0.1.0"
  }
}
$ npm install

とかで使えるようにした後、

const sillyname = require('sillyname');

for (let i = 0; i < 8; ++i) {
  console.log(sillyname());
}

こんな感じの適当スクリプトを走らせます。

$ node hoge.js

とかして実行すると、

Zestrazor Storm
Cedarspirit Drop
Shortbraid Leader
Violetraptor Hunter
Plumebrow Skinner
Satinraven Flier
Ceruleanbone Stork
Saberscowl Crown

とか

Typhoonpiper Shield
Vividflame Pirate
Peppermintfairy Rib
Bushpig Fly
Atomlasher Chiller
Weedstorm Sting
Inkwhale Tracker
Clovertail Jester

とか適当に表示されます。
結果が毎回異なることからわかるように、ランダムに出力されます。
SquidTracks は毎回こうやって普通に使っているので、毎回異なった名前になっています。

じゃあ stat.ink はどうして毎回同じになるのかというと、もちろん秘密があります。
まず、sillyname のソース を見ましょう。たったの 29 行です。

実装の詳細はどうでも良いのでとりあえず randomNoun() を見てみます。
すると、 generator = generator || Math.random; という行があって、変数 generator は optional な引数のようです。
省略時は Math.random、つまり [0, 1) な実数を返す乱数生成機です。

ソースを眺めると、どうも generator として [0, 1) な何かを返すジェネレータ関数をあたえてやれば面白そうな気がします。
ということで、こんなことをやってみます。

const sillyname = require('sillyname');

const generator = () => 0.5;

for (let i = 0; i < 8; ++i) {
  console.log(sillyname(generator));
}

generator として「常に 0.5 を返す乱数(?)生成機」を与えてみました。(JSのアロー関数を見慣れていないと難しいような気もしますが、 const generator = function () { return 0.5; }; と(この場合は)等価です)

これを実行してみると、

Scratchwolf Wolf
Scratchwolf Wolf
Scratchwolf Wolf
Scratchwolf Wolf
Scratchwolf Wolf
Scratchwolf Wolf
Scratchwolf Wolf
Scratchwolf Wolf

何回呼び出しても、何度実行しても同じ名前を生成するようになりました。
stat.ink はこの generator を利用して名前を「安定」させています。

もうここまででほとんど充分なのですが、さらに詳細に入ります。

まず、前提条件として、stat.ink は SquidTracks、splatnet2statink から「イカを特定する情報」をもらっています。
これは、イカリング2のAPIに含まれている値で、principal_id と呼ばれています。

stat.inkではこの値を直接は使わずにゴチャゴチャ変換をかけていますが、とりあえず principal_id というものがあります。
例えば「Cliffshaker Sting」さんの principal_id を一定のルールに従って変換したものが「34345676f8a35f579c595cd432bbd02beda1b8a8」です。

sillyname では 3 回 generator を実行して名前を得ますが、とりあえずこの値を 4 文字ずつに区切って整数にし、さらに [0, 1) の実数に変換します(具体的には 0x10000 = 65536 で割ります)。
(なぜ 4 文字なのかというと、ほどほどの精度が得られるから以上の意味はないです)

  • 3434 : 0x3434 = 13364 → 13364 / 65536 = 0.20391845703125
  • 5676 : 0x5676 = 22134 → 22134 / 65536 = 0.337738037109375
  • f8a3 : 0xf8a3 = 63651 → 63651 / 65536 = 0.9712371826171875
  • 5f579c595cd432bbd02beda1b8a8 – 残りはゴミ

sillyname の内部コードとこの数字に従って、とりあえずこんなコードを実行してみます。

const sillyname = require('sillyname');

const generator1 = () => 0x3434 / 0x10000;
const generator2 = () => 0x5676 / 0x10000;
const generator3 = () => 0xf8a3 / 0x10000;

console.log("noun1 = " + sillyname.randomNoun(generator1));
console.log("noun2 = " + sillyname.randomNoun(generator2));
console.log("adjective = " + sillyname.randomAdjective(generator3));

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

noun1 = shaker
noun2 = sting
adjective = Cliff

実際の sillyname を一発で呼び出した時、「adjective + noun1 + " " + noun2 (ただし noun2 の先頭は大文字にする)」で構成されるので、無事「Cliffshaker Sting」の名前が得られました。

ところで、実際には generator に即値をかくわけにはいきませんし、generator を毎回 3 つつくって呼び出すのもめんどくさいです。
実際のソースはこの通りなのですが、とりあえず再現してみることにします。

実際のソースでは jQuery.each でループを回して処理していますが、ここでは上の試験実行と同じく Cliffshaker Sting さんだけ固定でやってみます。
それ以外の点では、変数名 (j → index を除く) を含めてほぼそのままです。

const sillyname = require('sillyname');

const hash = '34345676f8a35f579c595cd432bbd02beda1b8a8';

let index = 0; // 何回目の generator の呼び出し?
const generator = () => {
  const hashVal = hash.substr(4 * index, 4); // 4 文字ずつ抜き出す
  const val = parseInt(hashVal, 16); // 16進文字列を数字に変換する
  ++index;
  return val / 0x10000; // [0, 1) に変換する
};

console.log(sillyname(generator));

これを実行すると、無事「Cliffshaker Sting」の名前が得られます。
めでたしめでたし。