2018年2月19日月曜日

フリゲ紹介: ロストアルター

ロストアルターはふりーむで公開されていたアクションゲーム。 タイトル画面やゲーム画面が面白そうだったのでプレイしてみて、短編ながら面白いと思った作品。



主人公 (下) を強化しながら探索をしていくショット主体のシンプルなアクションゲームで、 大きな特徴がある訳でもないのですが、操作性が良く、難し過ぎず、簡単過ぎず、サクサクとレベルアップできるのが気持ち良い。 プレイして気持ちが良いというのはやはり重要だと思います。



私のプレイ時間は30分もないですが、クリアまで30分も掛からず、これだけ楽しめるなら非常に良い作品だと思いました。

2018年2月17日土曜日

フリゲ紹介: 精霊の剣

精霊の剣は、Nepheshelイストワールに似た探索RPG。 かなり昔にプレイした作品ですが、あまり知られていないので紹介してみます。



Nepheshelやイストワールのような練られたストーリーがある訳ではなく唐突に世界崩壊スタートする作品 (左下)で、 その世界崩壊を食い止める探索を目的としたゲームです。 探索や戦闘はシンプル (右下) ながら面白い。物語を進めていくと様々な精霊を仲間にでき、徐々に面白みが増してきます。



Nepheshelやイストワールっぽいゲームの中では有名どころを除けばかなり上位に入ってくる面白さだったので、是非プレイしてみて欲しい作品です。

2018年2月16日金曜日

強力なポップアップブロックをChrome拡張で作ってみた (2)

強力なポップアップブロックをChrome拡張で作ってみた (1) の続きです。

前回は若干使いにくかったスクリプトの改善案として挙げた、(3) URLを判定したポップアップの停止について今回は話します。 もう少し具体的に言うと、閲覧中のページとはホストの異なるサイトを勝手に開こうとした場合、 それは鬱陶しいポップアップである可能性が非常に高いはずです。 そのようなポップアップだけ削除すれば、快適なブラウジングができると考えられます。

ただしこれを実現する場合、前回のファイルに多少修正を加える必要があります。 manifest.jsonを以下のように編集します。 まずは "run_at": "document_start" の指定でサイトのスクリプトより早く実行できるようにします。
{
  "name": "Minimal User CSS & JavaScript",
  "version": "0.0.1",
  "manifest_version": 2,
  "description": "Minimal User CSS & JavaScript",
  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "css": ["user.css"],
      "js": ["user.js"],
      "run_at": "document_start"  <-- 追加
    }
  ],
  "web_accessible_resources": ["popup_blocker.js"]
}

user.jsではscriptタグを付ける場所をbodyから文書の先頭に変更します。 これは "run_at": "document_start" で呼び出した際にはDOMの解析は終わっていても構築が終わっていないため、bodyタグが存在しないからです。
function loadScript(file, tag) {
  // var node = document.getElementsByTagName(tag)[0];
  var s = document.createElement('script');
  s.setAttribute('type', 'text/javascript');
  s.setAttribute('src', file);
  // return node.appendChild(s);
  return document.documentElement.appendChild(s);  <-- 変更
};
loadScript(chrome.runtime.getURL('popup_blocker.js'), 'body');

popup_blocker.jsではURLを判定したポップアップの停止をできるようにします。 これにはlocation.replaceやwindow.openといったURLを開くための関数を上書きする必要があり、 以前とは異なりオブジェクトの凍結や削除では不可能です。 サイトのスクリプトより早く関数の中身を変えておく必要もあり、これは以下のように書く事ができます。 window.openを上書きされないようオブジェクト化しておいて独自関数のcallで呼び出すのがポイント。
Object.freeze(document.location);
// delete window.open;

var window_open = window.open;
window.open = function(url_str, name, options) {
  try {
    url = new URL(url_str);  // not support IE
    if (location.host == url.host) {
      return window_open.call(window, url_str, name, options);
    } else {
      if (confirm('window.open [ '+ url_str + ' ], ok?')) {
        return window_open.call(window, url_str, name, options);
      }
    }
  } catch(e) {  // 同ドメインのURLパラメータなど
    return window_open.call(window, url_str, name, options);
  }
};

あとはこの拡張を読み込むだけです。chrome://extensions にアクセスし、上部に表示されている「デベロッパーモード」をONにします。 そして「パッケージ化されていない拡張機能を読み込む...」を押して、usercssjsディレクトリを選択すれば完了です。 普通の拡張のようにパッケージ化していないので、Chrome拡張のファイルを編集後に設定をリロードする事ができます。


ホスト判定だとGoogleのようにたくさんのホスト名を持っているサイトでは誤爆の危険性はありますが、 ホストが異なる時には開くかどうかの確認画面を出すようにしたので、 何の警告もなしに停止する2行のスクリプトよりは使いやすいかも知れません。

ほとんどのページで問題なく動いているのですが、たまに動かなくなるページがありますね。 例えばBloggerのプレビューボタン。 これは一体どういう処理をしているのだろう…。
返り値を返すように修正した事で今のところ手元の環境ではすべてのページで動いていますが、動作不良はあるかも知れません。(最終更新: 2018-10-06)


ところでこのスクリプトを作ってみてわかったのですが、 location.assignやlocation.replaceといったメソッドはoverrideできないようで、 できるのはせいぜいlocation.hrefの凍結という事がわかりました。 つまりポップアップは防げるけどリダイレクトは防げないという事のようです (駄目じゃん…)。

さらにこのスクリプトだけではiframeを利用したケースに対応できていません。 iframeと組み合わせたポップアップはクリックジャッキング対策をさらに施す必要があるようです。

細かな動作不良が気になる方は前回書いたようなサイトごとの適用のほうが良いかも知れません。 より使いやすい解決案が浮かんだら追記していこうと思います。

2018年2月15日木曜日

強力なポップアップブロックをChrome拡張で作ってみた (1)

Chrome 64からポップアップブロックが強化されたとの事で、さっそく入れてみました。 怪しげなサイトをいくつか開いて見ましたが、全然ブロックしてくれませんね…。少し残念。 これでは当分ポップアップとの戦いは終わらなそうです。

鬱陶しいポップアップやリダイレクトは、昔ならクリックジャッキングで実装している事が多かったと思うのですが、 最近はクリックイベントにスクリプトを仕込んでいる事が多いと思います。 そのためイベントが実行された後にブロックするのはスクリプトのスコープを覗けない以上、削除するのは割と難しいんですよね。 下手にイベントを削除するとサイトがまともに動かなくなる可能性もありますから。

しかし、ふと思いました。 そもそもJavaScriptでURLを開くイベントだけを削除してしまえば良いじゃないかと。 それなら悪影響もほとんどなくポップアップと確実にオサラバできるのではないかと。 そんな訳でChrome拡張を作ってみました。


まず以下のファイル構成を作ります。 以前の Minimal User CSS & JavaScript なChrome拡張を作った を拡張していく形で話を進めるので、合わせて参考にしてみてください。
usercssjs/
  manifest.json
  user.css
  user.js
  popup_blocker.js

manifest.jsonを以下のように編集します。 web_accessible_resourcesを指定してuser.jsからpopup_blocker.jsを呼び出せるようにするのがポイントです。
{
  "name": "Minimal User CSS & JavaScript",
  "version": "0.0.1",
  "manifest_version": 2,
  "description": "Minimal User CSS & JavaScript",
  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "css": ["user.css"],
      "js": ["user.js"],
    }
  ],
  "web_accessible_resources": ["popup_blocker.js"]
}

user.jsにpopup_blocker.jsを呼び出すための具体的なコードを加えます。適当な場所でOK。
function loadScript(file, tag) {
  var node = document.getElementsByTagName(tag)[0];
  var s = document.createElement('script');
  s.setAttribute('type', 'text/javascript');
  s.setAttribute('src', file);
  return node.appendChild(s);
};
loadScript(chrome.runtime.getURL('popup_blocker.js'), 'body');

popup_blocker.jsではJavaScriptでURLを開くイベントを削除します。 document.locationを凍結してページ移動を禁止し、window.openを削除する事で新規タブやポップアップを禁止します。 実はこれたったの2行で済みます。
Object.freeze(document.location);
delete window.open;

あとはこの拡張を読み込むだけです。chrome://extensions にアクセスし、上部に表示されている「デベロッパーモード」をONにします。 そして「パッケージ化されていない拡張機能を読み込む...」を押して、usercssjsディレクトリを選択すれば完了です。 普通の拡張のようにパッケージ化していないので、Chrome拡張のファイルを編集後に設定をリロードする事ができます。結構便利。



このスクリプトを使うとポップアップの鬱陶しいサイトの動きを完全に止める事ができます。 リダイレクトは気持ち程度。 …ただ、身近なサイトでもwindow.openは結構使ってるので弊害があります。 例えばBloggerの編集画面は動かない場所が出てきました。

そんな訳で実利用に用いるにはもう少し改良を加えたほうが良いです。 (1) manifest.jsonで適用サイトを限定する、 (2) popup_blocker.jsで適用URLを限定する、 (3) URLを判定してポップアップを停止する、といった対処をしていく必要がありそうです。


1に関しては、manifest.jsonの "matches": ["<all_urls>"] を編集すれば良い(参考)ですが、拡張全体に影響が出てしまうので別の拡張に分離したほうが良いでしょう。 2に関しては、popup_blocker.jsを以下のようにホストで判定して適用すると良いでしょう。
if (location.host == "www.google.co.jp") {
  Object.freeze(document.location);
  delete window.open;
}

3に関しては、ここまでの記述にも修正が加わり、長くなりそうなので次回に続きます。

2018年2月14日水曜日

Deep Learningと回帰は相性悪い

ふとDeep Learningの回帰は非常に性能が低い事に気付きました。 少し考えてみると、そもそもDeep Learningは(単純な)回帰にはあまり向いてない事がわかります。

これは結構簡単に確認できます。 例えば mnist_mlp.py を10クラス分類で解くのではなく、0.0〜9.0の数値として捉えて回帰予測するタスクに変換してみます。これは以下のように書き換えればOK。
# y_train = keras.utils.to_categorical(y_train, num_classes)
# y_test = keras.utils.to_categorical(y_test, num_classes)
y_train = y_train * 1.0
y_test = y_test * 1.0

model = Sequential()
model.add(Dense(512, activation='relu', input_shape=(784,)))
model.add(Dropout(0.2))
model.add(Dense(512, activation='relu'))
model.add(Dropout(0.2))
# model.add(Dense(num_classes, activation='softmax'))
model.add(Dense(1))

# model.compile(loss='categorical_crossentropy', optimizer=RMSprop(), metrics=['accuracy'])
model.compile(loss='mse', optimizer=RMSprop(), metrics=['accuracy'])

この精度がどうなるかというと、10%も出ない事が確認できると思います。

なぜこうなるか。 逆伝搬の際に微分できない出力を与えた事により、学習器は微分できないものを頑張って学習しようとします。 しかしどう頑張っても微分できないので、10クラス中1クラスだけのフィッティングで諦める事になるため、概ね10%の精度になるという仕組みです。 このような仕組みを考えていくとDeep Learningの回帰って微妙ですよね。

もちろんこの辺を考慮した設計をすれば良い感じに回帰できるので、あくまで単純な回帰は微妙という事です。 こういった部分もまるっと学習してくれる技術が出ると面白いんですけどね。

2018年2月1日木曜日

JavaScriptで文字列をクリップボードにコピー (モバイル対応版)

JavaScriptで任意のオブジェクトをクリップボードにコピーする際には、以下のコードを使うのが定跡です。 要約すると、オブジェクトの文字列を一時的なtextareaにコピーして、そのtextareaを選択状態にした後、クリップボードにコピーしている訳です。
ただこのコードはiOS 10以降で動かなくなっている事に気付きました。 以下のコードエリアには同じスクリプトを適用してあるので、クリックして実際にコピーできるか確かめてみてください。
$('#pre1').click(function(){
  var input = document.createElement('textarea');
  document.body.appendChild(input);
  input.value = this.innerText;
  input.select();
  document.execCommand('copy');
  document.body.removeChild(input);
  alert('copied!');
});

確認するのが面倒な方のために具体的にどのような問題が起きるか説明すると、 (1)クリックした瞬間にフォーカスがあらぬ方向に吹っ飛び、(2)クリップボードへのコピーもできない状態になります。
この時一体どのような事が起きているかというと、 (1)iOS 10以降ではtextareaが書き込み可能状態だとフォーカスが移ってしまい、 (2)textareaの編集が禁じられているため選択状態にできず、クリップボードへのコピーに失敗しているのです。

この問題を解決するにはそれらのオプションを解除する必要があります。 StackOverflow にドンピシャの回答があったので載せてみます。
function copyToClipboard(el) {

    // resolve the element
    el = (typeof el === 'string') ? document.querySelector(el) : el;

    // handle iOS as a special case
    if (navigator.userAgent.match(/ipad|ipod|iphone/i)) {

        // save current contentEditable/readOnly status
        var editable = el.contentEditable;
        var readOnly = el.readOnly;

        // convert to editable with readonly to stop iOS keyboard opening
        el.contentEditable = true;
        el.readOnly = true;

        // create a selectable range
        var range = document.createRange();
        range.selectNodeContents(el);

        // select the range
        var selection = window.getSelection();
        selection.removeAllRanges();
        selection.addRange(range);
        el.setSelectionRange(0, 999999);

        // restore contentEditable/readOnly to original state
        el.contentEditable = editable;
        el.readOnly = readOnly;
    }
    else {
        el.select();
    }

    // execute copy command
    document.execCommand('copy');
}

この関数を使うとJavaScriptでクリップボードにコピーするコードは以下のように書く事ができます。 以下のコードエリアには同じスクリプトを適用してあるので、クリックして実際にコピーできるか確かめてみてください。
$('#pre2').click(function(){
  var input = document.createElement('textarea');
  document.body.appendChild(input);
  input.value = this.innerText;
  copyToClipboard(input);
  document.body.removeChild(input);
  alert('copied!');
});

ところでdisplay none; を付ければフォーカスを移動せずに済むように思うかも知れませんが、 これはtextareaを選択状態にする事ができませんでした。 また文字列ではなく画像をクリップボードにコピーするのも駄目なようです。

StackOverflowを参照すれば解決できる問題ではあるのですが、 再び仕様が変化した時にどのように対応すれば良いのか思い出しやすいように記事にしてみました。 JavaScriptは時間が経つと急に動かなくなったりするので大変です…。