Tauriで詰碁練習アプリを作ってみた
詰碁とは
詰碁(つめご)とは、囲碁の部分的な死活を問う問題のこと。将棋の詰将棋に対応するもの。
―― https://ja.wikipedia.org/wiki/詰碁
とのことです。
今回は、自分だけの「詰碁集」を作り、好きなように練習できるようにするためのデスクトップアプリを作ってみました。
ちなみにsinogiは囲碁用語から来ています。
補足
といっても、囲碁が分からないと何を作ったのかが分かりづらいため、「中学数学で取り扱う証明問題」に喩えて少しだけ説明します。
***
下記のサイトにもある通り、図形に関するものだけでも様々なパターンの問題があります。
そして高校入試・大学入試では、決められた時間の中で、どのパターンに属する問題なのかを判断し、仮定や定理を駆使して答えを導くことになります。
そのためには、予め
- 証明に使えるテクニック(条件や性質)を理解し、自由に使えるようにしておく記憶力
- 問題から自分が知っているどのパターンに近いのかを類推する想像力
- 仮定や定理を組み合わせて利用する思考力
を鍛える必要があり、それを鍛えるためには、問題集で多くの問題に触れ、繰り返し解き、様々なパターンを自分のモノにしていく必要があります。
また、苦手なパターンがあれば集中的に練習する必要があります。
***
囲碁においても同様に、局所的な場面(局地戦)で勝負を有利に進めるためには、このような力が必要になってきます。
そして、それを鍛えるためには「詰碁集」という問題集を使って、ひたすら解いていくことが不可欠です。
使用技術スタック
今回はとりあえず個人のパソコンで使えれば良かったため、Tauriを使って作成してみました。
Windows/Mac/Linux用のデスクトップアプリを作れるフレームワークです。 将来的にはモバイルにも対応するようです。
同様のフレームワークにはElectronもありますが、メモリ使用量やバイナリサイズの点で有利なため、Tauriにしました。
UI部分の構築にはWebフロントエンド技術が使えるため、ReactとAnt Designを使い、TypeScriptで書いています。
また、登録した詰碁問題や解いた履歴を保持しておくために、データベース(SQLite)も使用します。
できたもの
こんな感じのアプリになりました。
アプリの使い方は、別途ユーザーズガイドに記載しています。
ソースコードはこちらにあります。
使ってみて良かったこと
Rustに詳しくなくても作れる
公式ドキュメントが充実しており、ドキュメントにあるサンプルコードを応用すればRustのコードをそこまで書かなくても作れてしまうことは驚きでした。
今回Coreプロセスで担当した処理は
ですが、これら全てのことが、たったの120行程度のコードで実現できています。
Tauriのデメリットとして、フロントエンド技術に加えRustも学ぶ必要がある点が挙げられますが、そこまでRustに精通していなくとも開発できるように工夫されている印象を受けました。
APIやプラグインが充実している
前述した部分に重なる部分がありますが、Tauriは標準で様々なAPI(Tauri API)やプラグインを提供しており、WebViewプロセスから直接呼び出して使うことができます。
今回はファイル選択ダイアログを使いましたが、下記のようなコードだけで実現できます。
import { open } from '@tauri-apps/api/dialog';
const selectedFile = (await open({
filters: [
{
name: 'SGFファイル',
extensions: ['sgf'],
},
],
})) as string | undefined;
if (!selectedFile) return;
console.debug(selectedFile);
// --> C:\Users\tkzwhr\Desktop\example.sgf
他にも、データベースやキーバリューストアなど、標準ではないが使用頻度の高い機能もプラグインとして提供されているため、かなり高速に開発ができました。
Tauriでの開発に使えそうなリソースがまとまっているページがあったためご紹介します。
使ってみて大変だったこと
ローカル環境でのクロスプラットフォームビルドに対応していない
現状ではローカル環境で別OS用のビルドを行うことはできず、開発環境を別途用意するか、Github Actionsを使うことになります。
特にWSLで開発している場合、Windowsでの動作確認は多少手間がかかるので注意です。
プラグインで問題が発生したときの調査が難しい
コンパイルエラーやランタイムエラーも発生せず、想定通り動かない状況に遭遇するケースがあります。
例えば、現状ではSQLで COUNT
を使うことができません。このようにシンプルな
SELECT count(*) AS count FROM table;
というクエリを実行しても、実行結果は以下のように null
が返却されるJSONになってしまいます。
[
{
"count": null
}
]
SQLのプラグインは内部的に sqlx というライブラリを使用しており、このライブラリを直接使用するケースにおいてもこの問題は発生しているようです。
他にも、IN
句に渡す値はパラメータバインディングではなく直接クエリに埋め込む必要があります。
const ids = [1, 2, 3];
// これでは動かない
await db.execute(
`
DELETE
FROM table
WHERE id IN ($1);
`,
[ids]
);
// こうすると動く
const idsString = ids.join(',');
await db.execute(
`
DELETE
FROM table
WHERE id IN (${idsString});
`
);
調べたところ、こちらもsqlxに関するもので、仕様とのことでした。
このような不可解な動作に遭遇したときは、プラグインだけでなくプラグインが利用するライブラリ(sqlxなど)に問題があるのかも含めて調査する必要があり、ある種の根気強さが求められます。
Tauri APIで対応できない場合は直接実装する必要がある
新しくウィンドウを開くとき、既にウィンドウが開かれていたら再利用する仕様を実装しようとしたのですが、これが少し難しかったです。
Tauri APIで実装するのであれば、
import { WebviewWindow } from '@tauri-apps/api/window';
const webview = new WebviewWindow('new-window', {
url: 'path/to/page.html',
});
といった感じでできそうな気がするのですが、 WebviewWindow
クラスにページを変更するようなメソッドが見当たりませんでした。
そこで、 listener
や emit
メソッドを使って、サブウィンドウ側でメインウィンドウがemitしたイベントを受け取る方法も試してみましたが、こちらも何故か listener
がイベントを受け取れずダメでした。
(逆にサブウィンドウがemitしたイベントをメインウィンドウ側で受け取ることはOKでした)
最終的にはTauri APIを直接使う事を断念し、Eventsを使って一度Coreプロセスを経由してサブウィンドウに指示を出す方式にしました。
Coreプロセス側でサブウィンドウを探し、見つからなければウィンドウを生成し、見つかればページ変更のJavaScriptコードを実行するようにして解決しました。
#[tauri::command]
async fn open_problem_view<R: Runtime>(handle: AppHandle<R>, problem_id: String, title: String) {
let url = format!("/problems/{}", problem_id.clone());
if let Some(window) = handle.get_window("problem") {
let js = format!("window.location.replace('{}');", &url);
let _ = window.eval(&js);
let _ = window.set_title(&title);
} else {
WindowBuilder::new(&handle, "problem", tauri::WindowUrl::App(url.into()))
.title(title)
.menu(Menu::new())
.always_on_top(true)
.build()
.unwrap();
}
}
なお、Rust側の Window
クラスにもページを変更するメソッドがないため、JavaScriptコード実行というトリッキーな方法を採っています。
Tips
Coreプロセス側にビジネスロジックを実装する場合、Tauri部分とは別のクレートにして実装したほうが良い
Tauriが依存しているクレートは重量級が多く、ビルドには非常に時間がかかります。
プロトタイプの時点ではビジネスロジックをCoreプロセス側に寄せて実装していたため、Coreプロセス側のテストコードの実行にかなり時間がかかる状態になってしまいました。
そこで、ワークスペースを活用し、ビジネスロジックは別クレートとして実装することにしました。
これにより、Tauri依存部分をビルドすることなくテストコードを実行できるようになり、テスト駆動開発が捗りました。
(最終的には、これとは違う理由でビジネスロジックをWebViewプロセス側に寄せたため、この問題は発生しなくなったのですが、経験したこととして残しておきます。)
まとめ
ということで、詰碁の練習ができるデスクトップアプリを開発してみました。
Tauriは(詰碁練習アプリも?)トラブルシューティングが難しく、まだまだ発展途上な感はありますが、ちょっとしたGUIツールをサクッと作れてしまう足回りの充実度はかなり魅力的に感じました。
この機会に試してみてはいかがでしょうか?
Discussion