📝

Tauriで詰碁練習アプリを作ってみた

2023/05/07に公開

詰碁とは

詰碁(つめご)とは、囲碁の部分的な死活を問う問題のこと。将棋の詰将棋に対応するもの。
―― https://ja.wikipedia.org/wiki/詰碁

とのことです。

今回は、自分だけの「詰碁集」を作り、好きなように練習できるようにするためのデスクトップアプリを作ってみました。

ちなみにsinogiは囲碁用語から来ています。

補足

といっても、囲碁が分からないと何を作ったのかが分かりづらいため、「中学数学で取り扱う証明問題」に喩えて少しだけ説明します。

***

下記のサイトにもある通り、図形に関するものだけでも様々なパターンの問題があります。

https://高校受験勉強法.com/benkyou/suugaku/syoumei.html

そして高校入試・大学入試では、決められた時間の中で、どのパターンに属する問題なのかを判断し、仮定や定理を駆使して答えを導くことになります。

そのためには、予め

  • 証明に使えるテクニック(条件や性質)を理解し、自由に使えるようにしておく記憶力
  • 問題から自分が知っているどのパターンに近いのかを類推する想像力
  • 仮定や定理を組み合わせて利用する思考力

を鍛える必要があり、それを鍛えるためには、問題集で多くの問題に触れ、繰り返し解き、様々なパターンを自分のモノにしていく必要があります。

また、苦手なパターンがあれば集中的に練習する必要があります。

***

囲碁においても同様に、局所的な場面(局地戦)で勝負を有利に進めるためには、このような力が必要になってきます。

そして、それを鍛えるためには「詰碁集」という問題集を使って、ひたすら解いていくことが不可欠です。

使用技術スタック

今回はとりあえず個人のパソコンで使えれば良かったため、Tauriを使って作成してみました。

https://tauri.app/

Windows/Mac/Linux用のデスクトップアプリを作れるフレームワークです。 将来的にはモバイルにも対応するようです。

同様のフレームワークにはElectronもありますが、メモリ使用量やバイナリサイズの点で有利なため、Tauriにしました。

UI部分の構築にはWebフロントエンド技術が使えるため、ReactAnt Designを使い、TypeScriptで書いています。

また、登録した詰碁問題や解いた履歴を保持しておくために、データベース(SQLite)も使用します。

できたもの

こんな感じのアプリになりました。

demo

アプリの使い方は、別途ユーザーズガイドに記載しています。

ソースコードはこちらにあります。

https://github.com/tkzwhr/sinogi

使ってみて良かったこと

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での開発に使えそうなリソースがまとまっているページがあったためご紹介します。

https://github.com/tauri-apps/awesome-tauri

使ってみて大変だったこと

ローカル環境でのクロスプラットフォームビルドに対応していない

現状ではローカル環境で別OS用のビルドを行うことはできず、開発環境を別途用意するか、Github Actionsを使うことになります。

特にWSLで開発している場合、Windowsでの動作確認は多少手間がかかるので注意です。

プラグインで問題が発生したときの調査が難しい

コンパイルエラーやランタイムエラーも発生せず、想定通り動かない状況に遭遇するケースがあります。

例えば、現状ではSQLで COUNT を使うことができません。このようにシンプルな

SELECT count(*) AS count FROM table;

というクエリを実行しても、実行結果は以下のように null が返却されるJSONになってしまいます。

[
  {
    "count": null
  }
]

SQLのプラグインは内部的に sqlx というライブラリを使用しており、このライブラリを直接使用するケースにおいてもこの問題は発生しているようです。

https://github.com/launchbadge/sqlx/issues/1524

他にも、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に関するもので、仕様とのことでした。

https://github.com/launchbadge/sqlx/blob/main/FAQ.md#how-can-i-do-a-select--where-foo-in--query

このような不可解な動作に遭遇したときは、プラグインだけでなくプラグインが利用するライブラリ(sqlxなど)に問題があるのかも含めて調査する必要があり、ある種の根気強さが求められます。

Tauri APIで対応できない場合は直接実装する必要がある

新しくウィンドウを開くとき、既にウィンドウが開かれていたら再利用する仕様を実装しようとしたのですが、これが少し難しかったです。

Tauri APIで実装するのであれば、

import { WebviewWindow } from '@tauri-apps/api/window';

const webview = new WebviewWindow('new-window', {
  url: 'path/to/page.html',
});

といった感じでできそうな気がするのですが、 WebviewWindow クラスにページを変更するようなメソッドが見当たりませんでした。

そこで、 listeneremit メソッドを使って、サブウィンドウ側でメインウィンドウがemitしたイベントを受け取る方法も試してみましたが、こちらも何故か listener がイベントを受け取れずダメでした。
(逆にサブウィンドウがemitしたイベントをメインウィンドウ側で受け取ることはOKでした)

最終的にはTauri APIを直接使う事を断念し、Eventsを使って一度Coreプロセスを経由してサブウィンドウに指示を出す方式にしました。

https://tauri.app/v1/guides/features/events

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コード実行というトリッキーな方法を採っています。

https://github.com/tauri-apps/tauri/discussions/5377#discussioncomment-3833989

Tips

Coreプロセス側にビジネスロジックを実装する場合、Tauri部分とは別のクレートにして実装したほうが良い

Tauriが依存しているクレートは重量級が多く、ビルドには非常に時間がかかります。

プロトタイプの時点ではビジネスロジックをCoreプロセス側に寄せて実装していたため、Coreプロセス側のテストコードの実行にかなり時間がかかる状態になってしまいました。

そこで、ワークスペースを活用し、ビジネスロジックは別クレートとして実装することにしました。

https://doc.rust-jp.rs/book-ja/ch14-03-cargo-workspaces.html

これにより、Tauri依存部分をビルドすることなくテストコードを実行できるようになり、テスト駆動開発が捗りました。

(最終的には、これとは違う理由でビジネスロジックをWebViewプロセス側に寄せたため、この問題は発生しなくなったのですが、経験したこととして残しておきます。)

まとめ

ということで、詰碁の練習ができるデスクトップアプリを開発してみました。

Tauriは(詰碁練習アプリも?)トラブルシューティングが難しく、まだまだ発展途上な感はありますが、ちょっとしたGUIツールをサクッと作れてしまう足回りの充実度はかなり魅力的に感じました。
この機会に試してみてはいかがでしょうか?

Discussion