株価予測練習ツール作成

株価予測練習ツールを作成したのでご紹介します。
ブログメニューの倉庫のなかに保管しました。

倉庫 – 株価予測練習ツール

Note

動作環境:Windows10、Google Chrome(FileSystem API対応)

本ツールは株職人、相場師朗氏の著書「一生モノの株のトリセツ」から着想を得ました。第一章「キャベツの千切り1000回、株の練習は3000回」では、チャートをたくさん見て、深く考え、工夫することがとても大切であると解説されています。私も見よう見まねで株の練習を始めてみたのですが、どうしても前日何を考えどのような予測をしていたのか忘れてしまいます。2、3個の銘柄なら覚えていられるかもしれませんが … もしかして監視対象銘柄のチャートはすべて暗記してしまうくらい読み込まないといけないのかな、と思ったりもしましたが、ちょっと無理っぽいです。そこで自分がどのような予測をしたのかを残せるようなツールを作成しました。

とはいうものの実際はただの表です。作っている最中に「あれ?ひょっとしてエクセルでいいのでは?」と元も子もないような考えが浮かんできたりしましたが「それはそれこれはこれ」と呪文を唱えながらなんとか完成させました。

このツール自体はあまりおもしろいものではないので今回はツールの紹介に加え、ツールを作成する過程で学んだことを書きたいと思います。

お題

ローカルPC上でファイルシステムを使用する

株の値動き予測(high/flat/low)とメモを保存する方法としてFileSystem APIを使うことにしました。ちなみにFileSystem APIは現在Google Chromeにだけ実装されているようです。

Filesystem & FileWriter API – Can I use…

FileSystem APIは以前Androidアプリを作ったときに使ったことがあったので、そのままいけるかと思いましたが、しょっぱなから引っ掛かりました。requestFileSystem()でエラー発生です。

window.requestFileSystem  = window.requestFileSystem || window.webkitRequestFileSystem;
var fsDirEntry;  // ファイルシステムのルートディレクトリ(DirectoryEntryオブジェクト)

/***
 * ファイルシステム初期化
 */
function initFileSystem(callback) {
  window.requestFileSystem(window.TEMPORARY, 5*1024*1024 /*5MB*/,
// cordovaの場合
// window.requestFileSystem(LocalFileSystem.PERSISTENT, 0,
    function(fs) {
      console.log('Opened file system: ' + fs.name);
      fsDirEntry = fs.root;
      if (callback) callback();
    },
    function(e) {errMsg(e, "requestFileSystem");}
  );
}

function errMsg(e, funcName) {
  var msg = "*** ERROR ***\n";
  msg += "[FUNC]  " + funcName + "\n";
  msg += "[CODE]  " + e.name + "(" + e.code + ")" + "\n";
  msg += "[DESC]  " + e.message;
//  alert(msg);
  console.log(msg);
}

window.onload = function() {
  // ファイルシステム初期化
  initFileSystem();
};

console出力

エラーコード18、SecurityErrorです。
Webで検索したところMDNのFileErrorのページで解説されていました。引用します。

Don’t run your app from file://

For security reasons, browsers do not allow you to run your app from file://. In fact, many of the powerful storage APIs (such as File System, BlobBuilder, and FileReader) throw errors if you run the app locally from file://. When you’re just testing your app, and you don’t want to set up a web server, you can bypass the security restriction on Chrome. Just start Chrome with the –allow-file-access-from-files flag. Use the flag only for testing purposes.

上記の引用を翻訳アプリで翻訳しました。

セキュリティ上の理由から、ブラウザではfile://からアプリを実行することはできません。実際に、File System、BlobBuilder、FileReaderなどの強力なストレージAPIの多くは、file://からローカルでアプリケーションを実行するとエラーが発生します。アプリをテストしているだけで、ウェブサーバーを設定したくない場合は、Chromeのセキュリティ制限を回避できます。 –allow-file-from-filesフラグを付けてChromeを起動してください。このフラグは、テスト目的でのみ使用してください。

具体的にどのようなケースを想定して禁止にしているのか知りたかったのですがちょっと調べられませんでした。Web上からダウンロードしたアプリをPC上で動かすとき、アプリがPC上のファイルを勝手にいじれないようにするためでしょうか?
サーバにファイルをアップしてそこでコーディングやデバッグをすれば問題なさそうですが、やっぱりローカルPC上で作業したいと思い、上記の解説どおり–allow-file-from-filesフラグを付けてChromeを起動し、ローカルPC上で作業することにしました。その手順を以下に記します。

ローカル開発専用のChromeのショートカットを作成する

「このフラグは、テスト目的でのみ使用してください」ということですので、開発中はフラグ付きで、通常時や開発終了後はフラグなしでChromeを使いたいところです。ツールをいじるときに毎回フラグを設定し、また戻す、というのはめんどくさいですしフラグ名もなんか長いということで、ローカルで開発を行うときだけ使用するChromeのショートカットを作成し、それにフラグを付けて使うことにしました。

ショートカットは、たとえば、

(1)タスクバーのChromeアイコンを右クリックしてポップアップメニューを表示させる
(2)その中からChromeアイコンを右クリックしてポップアップメニューを表示させる
(3)その中からプロパティを選択してダイアログを表示させる
(4)「ファイルの場所を開く」ボタンを押してエクスプローラを表示させる
(5)そこにあるchrome.exeファイルを右クリックもしくは右クリック+ドラッグしてポップアップメニューを表示させ、ショートカットを選択する

または、

(1)左下のスタートボタン(ウィンドウズのアイコン)をクリックしてメニューを表示させる
(2)「よく使うアプリ」や「G」のインデックスからGoogle Chromeを探してドラッグ&ドロップ

などの方法で作成できます。

ショートカットにフラグを設定する

作成したショートカットに適当な名前を付けてフラグ付きであることを忘れないようにします。次にショートカットを右クリックしてポップアップメニューを表示させ、プロパティを選択します。表示されたダイアログのリンク先にフラグを追加します。

実際の作業は、もともと記述されているchrome.exeへのパスの後に半角スペースを入れた後「–allow-file-access-from-files」をコピペします。

リンク先(例)

“C:\Program Files (x86)\Google\Chrome\Application\chrome.exe” –allow-file-access-from-files

起動時の注意点

フラグ付きとフラグなしのChromeを切り替えて使うとき、すでにChromeが立ち上がっている場合は、いったんすべてのChromeを終了させないとフラグが反映されません
とは言っても閲覧中または参照中のWebページを閉じたくないときってありますよね。そんなときはChromeの拡張機能OneTabがおすすめです。この場合に限らず普段使いとしても超おすすめで、Chromeに標準装備すべき機能だと思います。ぜひお試しください。

これでローカルPC上でもファイルシステムを使用できる環境ができました。

ラジオボタンのチェックをループを使わずに取得する

本ツールは3択のラジオボタン(high/flat/low)を株の銘柄の数だけ持っています。

Saveボタンを押したときにすべての銘柄についてラジオボタンのチェック項目を取得してファイルに保存したいと考えていました。

ここで、ラジオボタンのチェック項目を取得する単純なサンプルを示します。とりあえずラジオボタンのグループを3つ(A, B, C)用意しました。

HTML

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>radio button</title>
<script src="app.js"></script>
</head>
<body>
<div>
  <span>A:</span>
  <input type="radio" name="r0" value="0">high
  <input type="radio" name="r0" value="1" checked>flat
  <input type="radio" name="r0" value="2">low
</div>
<div>
  <span>B:</span>
  <input type="radio" name="r1" value="0">high
  <input type="radio" name="r1" value="1" checked>flat
  <input type="radio" name="r1" value="2">low
</div>
<div>
  <span>C:</span>
  <input type="radio" name="r2" value="0">high
  <input type="radio" name="r2" value="1" checked>flat
  <input type="radio" name="r2" value="2">low
</div>
<button id="check">チェック</button>
<span id="result"></span>
</body>
</html>

Script

window.onload = function() {
  var elem = document.getElementById("check");
  elem.addEventListener("click", checkValue);
};

一般的な方法で書くと以下のような感じでしょうか。forループ内でラジオボタンのchecked属性の値をif文で判定し、チェックされている要素を見つけます。

Script

function checkValue(e) {
  var price = ["high", "flat", "low"];
  var names = document.getElementsByName("r0");
  for (var i=0; i<names.length; i++) {
    if (names[i].checked) {
      var r0 = parseInt(names[i].value, 10);
      break;
    }
  }
  var names = document.getElementsByName("r1");
  for (var i=0; i<names.length; i++) {
    if (names[i].checked) {
      var r1 = parseInt(names[i].value, 10);
      break;
    }
  }
  var names = document.getElementsByName("r2");
  for (var i=0; i<names.length; i++) {
    if (names[i].checked) {
      var r2 = parseInt(names[i].value, 10);
      break;
    }
  }

  var elem = document.getElementById("result");
  elem.textContent = "A: " + price[r0] + ", " +
                     "B: " + price[r1] + ", " +
                     "C: " + price[r2];
}

上記をがんばってまとめると以下のように書けます。基本的な部分は同じですが、だいぶコンパクトになりました。

Script

function checkValue(e) {
  var price = ["high", "flat", "low"];
  var r = [], ROWS = 3;
  for (var j=0; j<ROWS; j++) {
    var names = document.getElementsByName("r" + j);
    for (var i=0; i<names.length; i++) {
      if (names[i].checked) {
        var value = parseInt(names[i].value, 10);
        break;
      }
    }
    r[j] = value;
  }

  var elem = document.getElementById("result");
  elem.textContent = "A: " + price[r[0]] + ", " +
                     "B: " + price[r[1]] + ", " +
                     "C: " + price[r[2]];
}

上記の方法でまったく問題ないのですが、あるときとてもいい感じの方法を思いつきました。

Script

function checkValue(e) {
  var price = ["high", "flat", "low"];
  var names = document.querySelectorAll("input[type='radio']:checked");
  var r0 = parseInt(names[0].value, 10);
  var r1 = parseInt(names[1].value, 10);
  var r2 = parseInt(names[2].value, 10);

  var elem = document.getElementById("result");
  elem.textContent = "A: " + price[r0] + ", " +
                     "B: " + price[r1] + ", " +
                     "C: " + price[r2];
}

querySelectorAll()のおかげでforループとif文がなくなりました。とても見やすくなったと思います。querySelector/querySelectorAllってけっこうなんでもありなんですね(誉め言葉)。

CSVファイルを扱う

今回CSVファイルを初めて扱いました。まず、扱ってみて思ったことを書いてみます。

  • CSVファイルといえばMicroSoft Excel。私の使っているバージョンは2013(15.0.4981.1000)。「名前を付けて保存」のとき「ファイルの種類」で「CSV(カンマ区切り)(*.csv)」を選択することができるが、保存するとBOM(byte order mark)が削除される。
  • HTML5 FileAPIのreadAsText()メソッドによりutf-8形式でファイルを読み込むとき、CSVファイルにBOMが付いていないと文字化けする。
  • メモ帳などでCSVファイルを保存し直すとBOMを付けてくれる。
  • CSVファイルの文字列中の改行コードをちゃんと扱おうとすると一気にたいへんになる。今回はパス。ツールでは文字列中の改行は取り除く。文字列をダブルクォーテーションで囲うことで行の終わりの改行と区別するみたいだけど、文字列中にダブルクォーテーションがあるケースも考えられるしCSVの仕様をよくわかっていないと危険かも。
  • 書き出す場合は項目をカンマでつなげて行の最後に改行コードをくっつけるだけなので簡単(たぶん)。

CSVファイルの文字列データを二次元配列に格納するコードはこんな感じになりました。上記で述べているように文字列中の改行には対応していません。

Script

var sysTbl = [];
app.createSysTbl = function(str) {  // strはcsvファイルの文字列データ
  sysTbl.length = 0;
  str = str.replace(/\r\n/g, "\n");  // 改行文字を"\n"に統一
  str = str.replace(/^\s+|\s+$/g, "");  // 端の空白や空行を削除
  var lines = str.split(/\n/g);  // 改行文字で分割

  lines.forEach(function(line, idx) {  // 一行づつ処理する
    sysTbl[idx] = new Array();  // 二次元配列を生成
    [].push.apply(sysTbl[idx], line.split(/,|\t/g));	// 一行のデータを区切り文字で分割し、それを二次元配列にpush
  });
};

CVSファイルに書き出すデータを作成するコードはこんな感じです。

Script

var content = "";
sysTbl.forEach(function(sd) {
  content += sd.join(",");
  content += "\r\n";
});

アップロードとダウンロード

CSVファイルのアップロードは<input type=”file”>から行っています。この要素をクリックするだけでファイル選択のダイアログが表示され、ローカルPC上のファイルにアクセスできます。便利ですね。ファイルを選択してダイアログを閉じるとchangeイベントが発生するのでイベントリスナでこれを受け取りアップロードの処理を行います。

Script

document.getElementById("upload")
  .addEventListener('change', function(e) {
  if (this.files.length) {
    app.upload(this.files[0]);
  }
  this.value = "";  // 同一ファイル選択時も処理を行う
});

app.upload = function(file) {
  var divs = file.name.split('.');
  if (divs[divs.length -1].toLowerCase() === "csv") {
    var fr = new FileReader();
    fr.readAsText(file, "utf-8");  // ファイルの読み込み

    fr.addEventListener('load', function() {  // 読み込み完了
      app.createSysTbl(fr.result);  // 読み込んだ文字列がfr.resultに
      app.createUI();  // 格納されるのでこれを使ってUIに表示
    });
    fr.addEventListener('error', function(e) {  // エラー処理
      errMsg(e.target.error, "readAsText");
    });
  }
};

ダウンロードはhtmlのa要素でDownLoadボタンを作成しdownload属性にファイル名を設定しています。スクリプトではCSVファイルのデータをBlob(Binary Large OBject)に変換し、そのオブジェクトURLをa要素のhref属性に設定します。このa要素のクリックイベントのリスナを抜けた後にダウンロードが開始されます。

HTML

<a id="download" target="_blank" download="stocktool.csv">DownLoad</a>

Script

document.getElementById("download")
  .addEventListener('click', function(e) {
  app.download();
});

app.download = function() {
  if (!FS_ENABLE) return;
  var len = sysTbl.length;
  if (len === 0) return;

  app.save(false);

  var content = "";
  sysTbl.forEach(function(sd) {	// 二次元配列をひとつの文字列にする
    content += sd.join(",");
    content += "\r\n";
  });

  var bom = new Uint8Array([0xEF, 0xBB, 0xBF]);
  var blob = new Blob([bom, content], {"type": "text/csv"});

  if (window.navigator.msSaveBlob) {	// IE
    window.navigator.msSaveBlob(blob, "stocktool.csv");
  } else {
    if (objectURL) {
      window.URL.revokeObjectURL(objectURL);	// 解放
    }
    var elem = document.getElementById("download");
    objectURL = window.URL.createObjectURL(blob);
    elem.href = objectURL;
  }
};

ファイルAPI

Cordovaのファイル操作プラグインcordova-plugin-fileを使うときに作ったソースをそのまま使おうとしたらエラーが2か所で発生したのでメモしておきます。

・FileErrorが定義されていないというエラーが発生(Uncaught ReferenceError: FileError is not defined)。FileErrorは廃止されたようです。(Cordovaプラグインは大丈夫?)

Note: This interface is obsolete per the latest specification. Use the new DOM4 DOMError interface instead.

上記の引用を翻訳アプリで翻訳しました。

注意:このインタフェースは、最新の仕様では廃止されています。新しいDOM4 DOMErrorインタフェースを代わりに使用してください。

以下のサイトでも説明されています。
Remove FileError interface (deprecated) – chromestatus.com

・window.requestFileSystem()の第一引数にLocalFileSystem.PERSISTENTを指定していたところLocalFileSystemが定義されていないというエラーが発生(Uncaught ReferenceError: LocalFileSystem is not defined)。window.LocalFileSystemはAndroidには実装されてるけどGoogle Chromeには実装されていないってことでしょうか?とりあえず今回は第一引数をwindow.TEMPORARYに変更しました。

・ファイルの読み込み、書き込みについては何もいじらなくても動きました。

最後にFile System API/File APIのサンプルコードを載せます。コメントはあったほうがいい派なので残しました。なお動作の保証はできませんのでご了承ください。

Script

window.requestFileSystem = window.requestFileSystem ||
                           window.webkitRequestFileSystem;
var fsDirEntry;  // ファイルシステムのルートディレクトリ(DirectoryEntryオブジェクト)

/***
 * ファイルシステム初期化
 */
function initFileSystem(callback) {
  window.requestFileSystem(window.TEMPORARY, 5*1024*1024 /*5MB*/,
// cordovaの場合
// window.requestFileSystem(LocalFileSystem.PERSISTENT, 0,
    function(fs) {
      console.log('Opened file system: ' + fs.name);
      fsDirEntry = fs.root;
      if (callback) callback();
    },
    function(e) {errMsg(e, "requestFileSystem");}
  );
}

/***
 * ファイル読み出し
 * 指定したファイルのコンテンツをテキスト形式で読み出し、コールバックを呼び出す
 */
function readTextFile(path, callback) {
  fsDirEntry.getFile(path, {}, function(entry) {
    entry.file(function(file) {
      var reader = new FileReader();
      reader.readAsText(file);
      reader.onload = function() {
        callback(reader.result);
      };
      reader.onerror = function(e) {errMsg(e, "readAsText");};
    }, function(e) {errMsg(e, "file");});
  }, function(e) {errMsg(e, "getFile");});
}

/***
 * ファイル書き込み
 * 指定したコンテンツをファイルに書き込み、コールバックを呼び出す
 * ファイルが存在しない場合は、新しいファイルを作成する。
 */
function writeTextFile(path, contents, callback) {
  fsDirEntry.getFile(path, {create: true, exclusive: false}, function(entry) {
    entry.createWriter(
      function(writer) {
        var blob = new Blob([contents]);
        writer.write(blob);
        writer.onerror= function(e) {errMsg(e, "write");};

        if (blob.size < writer.length) {  // 書き込むデータのサイズが既存ファイルより小さい場合、
          writer.onwrite = function() {  // 書き込み終了後にファイルサイズを書き込みサイズに合わせる
            writer.truncate(blob.size);
            if (callback) {
              writer.onwrite = callback;
            }
          };
        } else {
          if (callback) {
            writer.onwrite = callback;
          }
        }

      }, function(e) {errMsg(e, "createWriter");}
    );
  }, function(e) {errMsg(e, "getFile");});
}

/***
 * ファイル追記書き込み
 * 指定したコンテンツをファイルに追記し、コールバックを呼び出す
 * ファイルが存在しない場合は、新しいファイルを作成する。
 */
function appendToFile(path, contents, callback) {
  fsDirEntry.getFile(path, {create: true, exclusive: false}, function(entry) {
    entry.createWriter(
      function(writer) {
        writer.seek(writer.length);  // ファイルの末尾に移動
        var blob = new Blob([contents]);
        writer.write(blob);
        writer.onerror= function(e) {errMsg(e, "write");};
        if (callback) {
          writer.onwrite = callback;
        }
      }, function(e) {errMsg(e, "createWriter");}
    );
  }, function(e) {errMsg(e, "getFile");});
}

/***
 * ファイル削除
 * 名前で指定したファイルを削除し、コールバックを呼び出す
 */
function deleteFile(name, callback) {
  fsDirEntry.getFile(name, {}, function(entry){
    entry.remove(callback, function(e) {errMsg(e, "remove");});
  }, function(e) {errMsg(e, "getFile");});
}

/***
 * ディレクトリ作成
 */
function makeDirectory(name, callback) {
  fsDirEntry.getDirectory(name, {create: true, exclusive: true}, callback, function(e) {errMsg(e, "getDirectory");});
}

/***
 * ディレクトリのコンテンツ読み出し
 */
function listFiles(path, callback){
  if (!path) getFiles(fsDirEntry);
  else fsDirEntry.getDirectory(path, {}, getFiles, function(e) {errMsg(e, "getDirectory");});

  function getFiles(dir) {
    var reader = dir.createReader();
    var list = [];
    reader.readEntries(handleEntries, function(e) {errMsg(e, "readEntries");});

    function handleEntries(entries) {
      if (entries.length == 0) callback(list);  // 完了
      else {
        for(var i=0; i<entries.length; i++) {
          var name = entries[i].name;
          if (entries[i].isDirectory) name += "/";
          list.push(name);
        }
        reader.readEntries(handleEntries, function(e) {errMsg(e, "readEntries");});
      }
    }
  }
}

/***
 * ディレクトリ削除
 */
function deleteDirectory(name, callback) {
  fsDirEntry.getDirectory(name, {}, function(entry) {
    entry.removeRecursively(callback, function(e) {errMsg(e, "removeDirectory");}); 
 // ディレクトリと、その中身をすべて削除する
  }, function(e) {errMsg(e, "getDirectory");});  // ファイルは消せない。エラー用コールバックが呼ばれる
}

/***
 * ファイル一括削除
 */
function deleteFiles(name, callback) {
  listFiles(name, function(list) {
    for(var i=0; i<list.length; i++) {
      deleteFile(list[i], callback);  // ディレクトリは消せない。エラー用コールバックが呼ばれる
    }
  });
}

/***
 * ファイル情報取得
 */
function getFileInfo(path, onSuccess, onFail) {
  fsDirEntry.getFile(path, {}, function(entry) {
    entry.file(onSuccess, function(e) {
      errMsg(e, "file");
      if (onFail) onFail();
    });
  }, function(e) {
    errMsg(e, "getFile");
    if (onFail) onFail();
  });
}

/***
 * エラー処理
 */
/*
function errMsg(e, funcName) {
  var info = errInfo(e);
  var msg = "エラーが発生しました!\n";
  msg += "[FUNC]  " + funcName + "\n";
  msg += "[CODE]  " + info.type + "\n";
  msg += "[DESC]  " + info.desc;
//  alert(msg);
  console.log(msg);
}
*/
function errMsg(e, funcName) {
  var msg = "*** ERROR ***\n";
  msg += "[FUNC]  " + funcName + "\n";
  msg += "[CODE]  " + e.name + "(" + e.code + ")" + "\n";
  msg += "[DESC]  " + e.message;
//  alert(msg);
  console.log(msg);
}

/***
 * FileErrorオブジェクトのエラーコードを文字列化する
 */
function errInfo(e){
  var inf = {};

  switch(e.code) {
    case FileError.ENCODING_ERR:
      inf.type = "ENCODING_ERR";
      inf.desc = "The URL is malformed. Make sure that the URL is complete and valid.";
      break;
    case FileError.INVALID_MODIFICATION_ERR:
      inf.type = "INVALID_MODIFICATION_ERR";
      inf.desc = "The modification requested is not allowed. For example, the app might be trying to move a directory"
               + " into its own child or moving a file into its parent directory without changing its name.";
      break;
    case FileError.INVALID_STATE_ERR:
      inf.type = "INVALID_STATE_ERR";
      inf.desc = "The operation cannot be performed on the current state of the interface object. For example,"
               + " the state that was cached in an interface object has changed since it was last read from disk.";
      break;
    case FileError.NO_MODIFICATION_ALLOWED_ERR:
      inf.type = "NO_MODIFICATION_ALLOWED_ERR";
      inf.desc = "The state of the underlying file system prevents any writing to a file or a directory.";
      break;
    case FileError.NOT_FOUND_ERR:
      inf.type = "NOT_FOUND_ERR";
      inf.desc = "A required file or directory could not be found at the time an operation was processed."
               + " For example, a file did not exist but was being opened.";
      break;
    case FileError.NOT_READABLE_ERR:
      inf.type = "NOT_READABLE_ERR";
      inf.desc = "The file or directory cannot be read, typically due to permission problems that occur after a reference to"
               + " a file has been acquired (for example, the file or directory is concurrently locked by another application).";
      break;
    case FileError.PATH_EXISTS_ERR:
      inf.type = "PATH_EXISTS_ERR";
      inf.desc = "The file or directory with the same path already exists.";
      break;
    case FileError.QUOTA_EXCEEDED_ERR:
      inf.type = "QUOTA_EXCEEDED_ERR";
      inf.desc = "Either there's not enough remaining storage space or the storage quota was reached"
               + " and the user declined to give more space to the database. To ask for more storage,"
               + " see Managing HTML5 Offline Storage.";
      break;
    case FileError.SECURITY_ERR:
      inf.type = "SECURITY_ERR";
      inf.desc = "Access to the files were denied for one of the following reasons:\n"
               + " ・The files might be unsafe for access within a Web application.\n"
               + " ・Too many calls are being made on file resources.\n"
               + " ・Other unspecified security error code or situations.";
      break;
    case FileError.TYPE_MISMATCH_ERR:
      inf.type = "TYPE_MISMATCH_ERR";
      inf.desc = "The app looked up an entry, but the entry found is of the wrong type."
               + " For example, the app is asking for a directory, when the entry is really a file.";
      break;
    case FileError.ABORT_ERR:
      inf.type = "ABORT_ERR";
      inf.desc = "FileAPI about error.";
      break;
    case FileError.SYNTAX_ERR:
      inf.type = "SYNTAX_ERR";
      inf.desc = "FileAPI syntax error.";
      break;
    default:
      inf.type = "UNKNOWN_ERROR";
      inf.desc = "FileAPI unknown error.";
      break;
  }
  return inf;
}

ここからダウンロードできます。
file.js

パズドラ練習アプリ/スタミナ管理ツール作成

パズドラの練習アプリとスタミナ管理ツールを作成したのでご紹介します。
ブログメニューの倉庫のなかに保管しました。

倉庫 – パズドラ練習アプリ
倉庫 – パズドラスタミナ管理ツール

練習アプリはJavaScriptを勉強し始めた頃に作ったもので、今回はソースの見直し、余分な機能の削除、コンボエフェクト追加、Edit機能の改善(作り替え)などを行いました。ベースはできていたのですぐ終わるだろうと思っていたのですが、当時JavaScriptの本を読みながら動かすことだけを目標に作っていたせいで、至る所にコメント(「クロージャとは?」とか「あとで!」など)や自由過ぎるグローバル変数が記述されていてたいへんでした。大抵プログラムには複数のフラグが必要になるわけですが、ちゃんとまとめておかないと後々えらいことになるということを実感しました。

スタミナ管理ツールはできたてホヤホヤです。練習アプリの公開のついでに作ってみました。グラフの作成にはこれまでflotr2を使っていたのですが今回インタラクティブにしたくて初めてChart.jsを使いました。コーディング的にはどちらも同じような感じでしたが、インタラクティブに対応しているせいかChart.jsのほうが華やかでオシャレ度が高いと思いました。プラグインで拡張もできるみたいです。わたし的にはインタラクティブ性が欲しい時はChart.js、軽く使いたい場合はflotr2という感じでしょうか。もっともグラフ・チャートを作成するJavaScriptライブラリはこの他にもたくさんありますから、機会があったら他のライブラリも使ってみたいと思います。

パズドラ練習アプリ

特徴

ノーマルモードで練習
操作時間を気にせずじっくりドロップの動きを確認できます。
なにがなんでもコンボをしなければいけないときに
Edit画面でパズドラの盤面と同じドロップ配置を作成し練習できます。

このアプリはコンボ吸収をする敵がでてきたときや4つ消し3コンボがしたい場合など、コンボを確実に決めなければいけないときに役立ちます。

想定している手順

  1. コンボをしなければいけないときがやってくる
  2. パソコンを立ち上げる
  3. パズドラ練習アプリのページをひらく
  4. Edit画面へ移動し、スマホを見ながらドロップを配置する(手動)
  5. Play画面に戻り、パズルと再配置を繰り返しながらルートを見つける(自力)
  6. 再生やパズルを繰り返し、見つけたルートをがんばって覚える(根性)
  7. パズドラに戻りチャレンジ

手間隙かかりますが、その分の見返りは期待できると思います。ただし練習し過ぎたせいで緊張したり途中で忘れて固まってしまうことも考えられます。もし途中でわからなくなったら無理に思い出そうとせず無心でパズルをした方がいいと思います(体験談)。

コンボの判定について

最初に思いついた方法は画像処理で使われる4連結ラベリングです。判定対象のドロップの色を白、それ以外の色を黒とした2値画像を作成し、ラベリング処理後、特徴量からコンボとみなすかどうかを判定する方法です。この方法で評価してみようかなと思ったのですが、肝心のラベリング処理と特徴量からのコンボ判定処理のコーディングがたいへんそうなことと、これら一連の処理をドロップの色の数だけループするのは処理速度的にどうかという疑問が浮かび止めました。
結局コーディングのしやすさ優先で処理単位ごとにモジュール化していったところ以下のようになりました。

  1. 水平方向に走査し横コンボを検出、上段との結合チェック
  2. 垂直方向に走査し縦コンボを検出、左段との結合チェック
  3. 横コンボと縦コンボを結合

おそらくもっといい方法があるはずです。例えば一回の走査で縦と横について処理を行ったり画像処理的なアプローチをしたりドロップの色ごとにテーブルを持ったりコンボの可能性のある部分(新しいドロップがセットされる周辺)だけ調べるなど。また記述についても、ループの数を減らすほうがいいのかループ内の判定文を減らすほうがいいのか、値を関数の引数で渡すのか関数内でget関数を使うべきかなど悩みどころはたくさんあります。とはいえ時間がかかりますしとりあえず問題なく動いているようなのでいったん終わりにして、機会があれば再考しようかなと思っています。またラベリングや特徴量抽出を行うライブラリを見つけたら使ってみたいです。

懸案事項

  1. パズドラでコンボ成立ドロップが消える順番が分からなかったのでとりあえずコンボを検出した順番にしました。
  2. スマホの場合ドロップが動かないことがありました。jQuery UIのdraggable機能を使っているのですが、start()メソッド後に頻繁に呼ばれるはずのdrag()メソッドが呼ばれない感じがします。PC上ではうまくいっています。またリロードすると大丈夫そうなので、ページの初回アクセスに起因するものと考えています。touch-punch.min.jsも関係あるのかもしれませんが謎です。

パズドラ スタミナ管理ツール

特徴

スタミナのたまり具合が一目瞭然
スタミナ50きざみで時間がわかります

パズドラの現在の仕様では、スタミナは3分間で1回復します。パズドラした後ってよく「3分で1だから1時間で20、5時間で100だから …」と計算しませんか?私はなぜか覚えられなくて毎回「3分で1だから」から始まります。
このツールは現在のスタミナを初期値として入力しグラフ作成ボタンを押すと、スタミナ回復の推移をグラフ化して表示します。いったんグラフを作成するといちいちパズドラにログインしなくてもスタミナを確認できます。
やってることは単にスタミナMAX値の到達時間を計算して直線を引いているだけです。データ点はちょっと工夫してスタミナ50きざみにしてみました。スタミナのカウントはパズドラと異なり時分に同期させました。タイマー10msでセンスしていますがDOMアクセスを時分単位に減らすためです。そのためパズドラのスタミナ回復時間の「あとxx:xx」の表示とぴったり合わせてグラフを作成したとしても誤差が生じることになります。誤差は計算上スタミナ1相当(±3分以内)のため実用上問題ないと考えています。

まとめ

どちらもHTML/JavaScriptで作っているのでそのままさくっとAndroidアプリにすることができるのですが止めました。さすがに数年遅いかなと。今だと7x6盤面とかコンボ加算スキルなどもでてきましたし。
そもそもAndroid上で使うためにはEdit画面でのドロップ配置の仕組み(スクショを撮ってドロップの色抽出から自動配置とか、パズドラの上に透過レイヤを載せて色を複写するとか?)を作らないといけなさそうでハードル高過ぎです。
今後は便利な機能やおもしろそうなアイデアが浮かんだらこっそり追加でもしようかなと思っています。