LoginSignup
4
1

More than 1 year has passed since last update.

ReactとTypeScriptとRustでWeb AssemblyなCPUと遊べるどうぶつしょうぎ™️を実装しました。

Last updated at Posted at 2023-01-31

スクリーンショット 2023-01-30 20.32.08.png

TL;DR

  • どうぶつしょうぎ™を作ったのでビルドとソースコードを公開しました。
  • Rustに入門してCPU処理をWeb Assembly化してみました。
  • 感想ポエムです。技術的には特に深い内容はないかも。評価関数の作り方については軽くご説明を添えました。

動作物

iPhoneSE2とChromeブラウザで動作確認済みです。CPUの「negamax 5」を選ぶと重いのでご注意を。。

全部CC0扱いで大丈夫です。そのうちライセンス表記整えておきます。

発端

webアプリ周りの技術を何年も触ってなかったので勉強しようかなと。
2022年末から暇を見つけて取り組んでみてました。

ReactもTypeScriptもRustもwasmも素人からの着手となりました。
なお、本記事はこれらについての学習経緯の感想ポエムの体裁となっております。それぞれの技術の詳細は解説しておりませんのでご容赦ください。

Reactは業務利用経験がない感じです。ちょっと触ったことはある、という程度の素人です。
今回はuseStateと関数コンポーネントによる初歩的なReactアプリの作り方を復習した感じですね。

TypeScriptもbetter javascript程度の使い方しか把握してない初心者です。まあ型システムの濃ゆいところは他の記事にお任せして……。もうTypeScriptの型システムとか闇すぎて何もわからん怖い

Rustは完全に初体験です(いや、一度hello worldをコンパイルしたことはあったかも?w)
今回はwasmへの興味も兼ねて合わせてRustに入門しておきたい気持ちがありました。採用部分としては、webアプリのCPUロジックをRustで記述して、Web Assembly(以下wasm)で実装した形になります。
「Rust難しいわー」って言葉もよく聞くので、噂の「Borrow Checkerさんとの戦い」を経験しておきたく。
結果いい言語だと感じましたが、確かに最初のハードルが高い言語かなぁ、とも。。

入門の序盤ではありますが、動くものができたのでここまでの所感をまとめました。
各技術に興味ある方の背中を押す助力になれば幸いです。

リポジトリと合わせて、wasmを利用した小物webアプリの実装事例としてご参考いただけるかなぁ……まあ、僕の実装よりわかりやすいサンプルは世の中にたくさんあるので、初心者による所感の記録として斜め読みして貰う程度で良いかなと。
入門時に参考にしたリンクも添えましたので、どなたかのお役に立てば嬉しいです。

ReactでUI作成

webアプリを作る際のトレンドは、最近だとReact、Anguler、Vueあたりが主流でしょうか。
生HTMLとJavaScript、CSSの組み合わせでも書けるものではありますし、Web Componentsも話題ですが、今回はReactを選びました。

本記事ではReact.jsの詳細は省きます。知名度も高いですし、公式サイトの情報量も十分です。僕が改めて説明する必要はないでしょう^^;

create-react-appを「--template typescript」で作業開始しました。普通ですね。

npx create-react-app animal-shogi-react-app --template typescript

最終的に出来上がったのは、以下のような画面のアプリです。冒頭のスクショの再掲ですね。

スクリーンショット 2023-01-30 20.32.08.png

この絵面でブラウザ閉じた人はいるだろうな
ご覧の通り、「デザイン? 何それ美味しいの」的なUIです。
本家どうぶつしょうぎ™の意匠を汚さぬよう、駒は絵文字で表現しております。

App.tsxの下に、3x4の盤面情報を表示するBoard.tsx、手駒情報を表示するコンポーネントを分割し、それぞれの1枠を表現するCell.tsxを共通で流用しています。

まあ、改めて「必要な情報は書いたze」止まりですね。エンジニアの美意識のなさが透けて見える画面というところでしょう。
最低限、UXの方はちょっと気にしたつもりではありますが。
操作できるコマを枠線で括い、駒を選択したら移動可能場所をハイライトしております。
どうぶつしょうぎ™のルールをご存知の方なら説明なくプレイできるのではないかと。

アニメーションも演出も一切考えてない最小構成でお送りいたします。

Reactの復習と実装について

まず今ReactでCSS扱うにはどうするのがいいんだっけ? ってあたりから調べてました。BEMはもう嫌です

Zenn: Reactにおけるスタイリング手法まとめ

こちらの記事を参考にさせていただきました。感謝。

今回は、create-react-appした直後の状態で使える「CSS Modules」で必要十分かなと判断しました。
ファイルが増えるくらいはそんなネガティブでもないかなと。あとはまあ、モジュール追加とかを避けて必要な学習コストの最小化ができるといいなと。

設計は雑です。ReactのHello Worldに毛が生えた程度かと。

useStateはApp.tsxに全部持たせて、盤面や盤面のセル表示コンポーネントは親にもらった情報で表示するだけのUIに徹しています。
こう割り切ると、Reactは楽ですね。ほぼ悩まず、機械的にコーディングできた印象です。

ただその結果として、App.tsxが太ったコードになったのでどうしたもんかなとは思いつつ……(´・ω・`;)

まあ、Reactのベストプラクティス学習は今後の課題とします。
(マサカリは歓迎です。お手柔らかにお願いしますね^^;

関数コンポーネントとフックはいいものですね。少ない記述で目的を十分に満たせます。
クラスベースのReactの時より随分スッキリしたなぁ。

本記事ではReactの詳細には踏み込みませんが、「最近React触ってないしフック触ったことないんだよなー」という方には以下の公式の解説をお勧めします。

フックの導入

Qiita記事からもひとつ。こちらもサクッと読める良記事でした。感謝。

jQueryでごちゃごちゃしたコードを書いてた時代を思い出すと色々感慨深いですね。 まあ比較しちゃいけないやつだとは思いますが

困ったらググれば何かしらの情報に出会える普及具合も素敵ですね。
おかげで特につまづきもなく、心穏やかに実装を進められました。

公式のどうぶつしょうぎ™アプリとの実装の違い

今回の実装のUX面について少々。
公式のどうぶつしょうぎ™アプリとの違いとしては、以下の特徴があります。

  • 有効手のみ洗い出し、操作可能な手として表示しています。
  • 相手のトライを防げない局面や、詰んでいる局面になったらその時点でゲームオーバーと判定します。
  • トライが狙われている際や王手の際は、回避できる手のみを有効手とし、それ以外の手は打てないようにしています。

なんでそんなめんどくさいことを? と思われるかもしれませんが、CPUターン処理を作る時には最初から着手可能手を絞り込んでおいた方が楽かなと。
「トライ成功 or キャッチ成功」でゲーム終了となる公式アプリのプレイ感とは変わってしまってますが、ちょっとだけ話の早いプレイ感になったかと思います。

TypeScript復習

TypeScriptも何気に一年以上触ってなかった感じです。軽く復習から……。

TypeScriptの型入門

今回はこちらの記事に助けられました。秀逸なまとめです。感謝。

ただまあ、先に予防線を張らせていただくと、TypeScriptの言語機能や型システムには深入りしてません^^;
便利そうな機能をふわっと使うにとどめてます。「JavaScriptを書きやすくする道具」という割り切った使い方ですね。

業務利用に必要になったらもう少し本腰を入れるとは思うんですがw
この言語、型システム周りに深入りすると急に理解が追いつかなくなるのが悩みです(´・ω・`;;)

先のリポジトリでも雑に書き散らしたコードがコミットされております。
予めご容赦くださいませ。

TypeScriptでのCPUターン処理の作成

申し遅れましたが、この記事ではコンピューターの手番処理のことを「CPU」と記載します。
普通にAIと呼ぶこともありますしもちろん間違いではないのですが、昨今はAIというと機械学習系の話題を指す言葉になってるので避けておきます。
(CPUってプロセッサだろ……という疑問もあるとは思いますが、おっさん的にはゲームのコンピューターターンは慣例でCPUって呼ぶのに慣れてしまっており。まあ昭和の名残と思って流してください(´・ω・`)

さて、今回は強いCPUターン処理を作ることには着目していません。
僕より強い程度。
今回は、コード的にシンプルなアルゴリズム「NegaMax法」による先読み探索を実装しました。alpha-beta枝刈りもしてない手抜きっぷり。

実装については以下の流れで進めました。

  • 最初に、着手可能手の一覧からランダムに打つCPUを作成する
  • 次に、評価関数を作成して、高いスコアの盤面状態になる手を選ぶCPUを作成する
  • NegaMax法でざっくり先読みするCPUを作成する

また、今回の実装では、任意タイミングでCPU種別を選べるようにしています。ドロップダウンリストからお好きなものを選んでお試しください。

この実装はデバッグにも役立ちました。
実装中も、任意のCPU同士で対戦させるなどを通してデバッグ効率に寄与してくれました。

ランダムCPUも着手可能な有効手のみ評価しているので、王手になれば逃げますし、あんまり適当に打ってると不意に負けたりしますのでご注意ください^^

評価関数版も、着手可能手の全部の状態を確認しているので勝利手を見逃さず、何気に気が抜けません。
未経験な方は、まずはこの子に勝てるようルール把握いただけるとお楽しみいただけるのではないかと。
三歳児くらいのお子様の知育にいかがでしょう^^;

接待プレイに徹するCPUも作ってみようかな

「評価関数」についてさらっと解説

先ほどさらっと「評価関数」という言葉を流しました。
盤面評価はUI実装の段階でも着手可能手の判定などで作成していますが、総じてこの評価関数を作成するための土台でもあり、本アプリの主体となっています。
ここではゲーム評価の仕組みを存じない方へのご案内も兼ねて、僭越ながら本作の評価周りについて、少し解説させていただきます。
大した深い話はしていないので、ご存知の方は読み飛ばしてくださいませ^^;

将棋やリバーシといった類のボードゲームで手を打つためには、盤面状態を評価する必要があります。
しかしコンピューターは、盤面の良し悪しを理解できません。

さてどうするか。まずは事前準備からご説明します。
今回は着手可能手を算出するコードから着手していますが、結構泥臭い処理が続きます。
UI実装のためにルールを判定するメソッドをそろえていく感じですね。

  • 駒がどの方向に進めるかを配列などの形で用意しておく
  • 初期盤面データを作成する
  • 初期盤面データから、お互いの盤面について、「効いている」(駒が動ける場所がある)場所の一覧を作成する → 王手判定や移動可能場所の情報として利用
  • 着手可能手の一覧を作成する
    • チェックメイトされていたら、回避手の一覧を作成する
    • トライ可能状態になっていたら、回避手の一覧を作成する
      • 持ち駒を打ったら回避可能になっている場合も判定する(※どうぶつしょうぎでは、トライしてキャッチされたら負けなので、持ち駒を配置した時に回避可能になるケースが存在する)
    • それ以外の場合は、可能な手の一覧を作成する
    • 持ち駒についても、すべての空白セルへの配置を着手可能手として判定する

なかなか泥臭いですが、データ構造を決めたらあとは手を動かすだけで実装可能な領域ですね。ユニットテストを並行して作成していけば怖くはありません。
将棋でもリバーシでも、このような「ルールを評価するコード」は必須です。
まずはUI表現とランダムAI(?)の実装が整うことを目標ということですね。
「着手可能手の一覧を作成する」ところまで手を動かし続けました。
ま、今回は勉強だし納期とかないから気持ちの面では楽なもんよ

ボードゲームによって判定すべき情報は様々ですが、今回のどうぶつしょうぎで判定したい処理は上記の情報で出揃った感じになりました。
あとはこれを元に、人間の手で、「計算によって盤面状態を点数化する」コードを書く必要があるわけです。

昨今の機械学習方面は全く考え方が違うAIが話題ですが、僕は詳しくない(理解できてない)ので割愛します^^;
本記事で扱うCPUは、昔ながらの「評価関数」を用いた実装です。

話を戻しますと、コードに落とし込むにあたり、何をもって「点数の良し悪し」を判断すべきでしょうか、という課題があります。
語弊を覚悟で結論を先に書きますと、割と直感ですw

例えばですが、リバーシを題材に取ると、良い盤面というのはなんでしょう。
まず言語化できるところとしては、

  • 自分のコマが多い方が良い
  • カドに自分のコマがある盤面はとても良い

というあたりでしょうか。

これを数字化するにあたり、以下の処理を考えます。

  • 自分のコマが多い方が良いので、自分のコマひとつあたり +1点, 相手のコマひとつあたり -1点とする
  • カドに自分のコマがある盤面はとても良いので +10点 とする。相手なら-10点。

はて。
……本当にこれだけで大丈夫でしょうか。

例えば今「10点」って書きましたけど……、本当にそんな点数でいいのか、と。
30点くらいが妥当なのでは……いやいや100点くらいの価値があったりしないか? と、人間様の頭脳で考えると、エビデンスのない「推測」を色々と考え込んでしまいます。

実のところ、「こんな感じ」でも、なんとなく動作するんですよね。
10点でもコンピューター計算としては「他のコマ9個取るよりカドを狙う」挙動を示すわけです。
「10点で判断させるのが最強か否か」はこの段階ではわからないのですが、CPUが「より良い状況を探る」傾向は作れるわけです。
これに勝利状態を含めた先読み系の探索を加えることで、思ったより強くなる……という感じです。

先の「10点」みたいな要素をパラメータとして、いろんなパラメータでCPU同士に戦わせることで、より強い評価関数パラメータを探ることもできますね。(※本記事ではそこまでやってません)

探索処理と評価関数は実に奥の深い世界です。
リバーシAIの世界に興味が向いた方は、下記Nyanyan_Cube様の記事が入口になるのではないでしょうか。秀逸なまとめです。

評価関数は盤面を一見して形勢を評価する関数です。オセロにおいては、多くの人がご存知のように隅のマスがとても強力に形勢に作用します。こういった知識を使って、マス自体に重みをつけてやれば、あっという間に評価関数が作れてしまいます。

いいまとめだなと思いました。もちろんその先は結構な伏魔殿なのですが。深い世界の入り口についてもご紹介されてます。……へえ、リバーシAIのコンテストなんてものがあるのか^^;

さて、改めて「どうぶつしょうぎ™」の盤面評価を考えます。
まあ、「感覚」を大事に、とりあえず書いてみましょう。

僕が今回実装した評価関数の、rust版の方のコードブロックを引用します。
すまないがTypeScriptの方はちょっとお目汚しな状態なのでね(´・ω・`)

// board.rsより引用
	pub fn calculate_score (&mut self, side:&Side) -> i32 {

		// 盤面状態評価
		self.get_or_create_valid_hands(side);
		
		// 勝敗状態を返却
		if self.states[side.to_index()] != SideState::Playable {
			return -99999;
		}

		// 点数計算開始
		let mut score = 0;

		// 盤上の駒の点数をside毎に評価
		for x in 0..3{
			for y in 0..4{
				let cell = self.cells[y][x];
				if cell.side == Side::Free { continue; }
				let is_own = if cell.side == *side { 1 } else { -1 };
				score += cell.koma.to_onboard_score() * is_own;
			}
		}

		// 手駒の点数を評価
		for tegoma in self.tegomas[side.to_index()].iter() {
			score += tegoma.to_tegoma_score();
		}
		for tegoma in self.tegomas[side.reverse().to_index()].iter() {
			score -= tegoma.to_tegoma_score();
		}

		// 着手可能手の多さを評価
		score += self.get_or_create_valid_hands(side).len() as i32 * ENABLE_MOVE_SCORE;

		// 効いてる場所の数を点数に加える
		score += self.get_or_create_attackable_map(side).count_flags(true) * ATTACKABLE_POS_SCORE;
		score -= self.get_or_create_attackable_map(&side.reverse()).count_flags(true) * ATTACKABLE_POS_SCORE;

		// Lionのトライ可能性評価で1ラインごとに加算
		score += self.get_lion_progress(side) * LION_LINE_SCORE;
		score -= self.get_lion_progress(&side.reverse()) * LION_LINE_SCORE;

		// チェックメイト時は一定点数加算
		// - この評価は番手のみ
		score += if self.get_or_create_is_checkmate(side) { CHECKMATE_SCORE } else { 0 };

		// 敵がトライ可能な時は一定点数減算
		// - この評価は番手のみ
		score -= if self.is_tryable(&side.reverse()) { TRYABLE_SCORE } else { 0 };
		
		return score;
	}
// constants.rsより引用
//ライオンが前に出た場合の1行あたりのスコア
pub const LION_LINE_SCORE:i32 = 140 ;

//「効く位置」いっこあたりのスコア
pub const ATTACKABLE_POS_SCORE:i32 = 30 ;

// 着手可能手一つあたりのスコア
pub const ENABLE_MOVE_SCORE:i32 = 30;

// トライ可能時のスコア
pub const TRYABLE_SCORE:i32 = 250;

// チェックメイト時のスコア
pub const CHECKMATE_SCORE:i32 = 200;

……これ、ここに晒すほどの価値あるコードじゃない気がするな(´・ω・`)
まあ、処理は追っていただけるのではないでしょうか。

まず重要なのは、初手で「勝利条件」の盤面状態を評価することですね。
今回の実装では、valid_hand評価時に有効手の状況を調べているので、SideState::Playableじゃなくなっていれば敗北状態の盤面となります。
ここでは勝利を99999、敗北を-99999という数字で表現しています。他の盤面スコアで到達できない桁の数であれば大丈夫です。(せめて定数化しとけよ俺

これでCPUは勝利手を見つけると迷わず打ってきますし、後述のNegaMax法のような探索処理を通すと「勝利手の多い方」「敗北手が少ない方」に偏ります。まずは勝敗条件を判断できることが重要なわけです。
UI段階で着手可能手の一覧作成まで完了していると、この辺の判定はグッと楽になります。作成済み・動作テスト済みのメソッドを呼び出すだけですから。

続くスコア評価については……
要するに「手駒の数」や「ライオンの位置」、「チェックメイトされているかどうか」……といった各種の「より良い状況とは何か」について思いついた要素を、「僕の直感で」点数として加えているだけです。

雑ですね。
しかし先ほどのリバーシの例えの繰り返しですが、実のところ、「こんな感じ」でもなんとなく動作するわけです。

今回の評価関数の考え方が本当に強いのかどうかについて、トライ評価に着目して振り返ってみましょう。

正直、いろいろ疑問が残るところです。

ここでは「ライオンが前に進む」たびに点数を加算しています。意図としては、「トライの可能性が高くなるので良い盤面だ」という「気持ち」を数字化したわけですが……。
この1ラインごとの追加点数の定数を変更することで、「このCPU、妙にトライ狙ってくるな?」というような挙動の変化につながります。

しかし実のところ、「トライ狙いが本当に強いのかどうか」について、僕はエビデンスを持っておりません。
自分が対局してみた印象として、「うーんこのCPU、トライ狙ってきてウザい(´・ω・`;)」と感じる程度には圧迫してくるので、よしとしています。
このように、評価関数の作成は、いわゆる業務ドメイン知識とでも言いますか……どうぶつしょうぎ™そのものに関する経験値が必要なところがあります。
どうぶつしょうぎ™に強く、一家言をお持ちの方が評価関数を書いたら、僕より強い評価関数パラメーターを見出すのではないかと思います。

人間の経験に頼らず強いパラメータを探す際は、機械的な学習が有用ですね。
例えばパラメータをランダム化してコンピュータ同士で戦わせて、より強いパラメータの傾向をグラフに落とし込むなどして調整できそうです。

NegaMax法について

NegaMax法については解説記事が豊富なのと、どうぶつしょうぎ™に直接関係のある技術でもないので、ざっくりとした説明に留めます。

要するに自分のターンの探索後、相手ターンの評価を符号反転して再帰処理の形で記述できる実装方法ですね。
NegaMaxは数ある探索実装の中でもコード量が少なく済むのが特徴です。探索速度の面で特徴があるわけではないのでご留意を。

あとは参考サイトなどを……と思うのですが、negamaxで検索したところ、下記のhiroi様の記事が良さげでした。理解の助けになるかもしれません。

「リバーシ アルゴリズム」などでGoogle検索していただけると、NegaMaxに限らず、多彩な手法の事例が見つかります。

あんまり強くない件と、実装不備に関するお詫び

さて、今回実装したNegaMaxなCPUの強さについてですが……実はそんなに強くないです。
一応、僕よりは強い様子ではありますが。

僕自信、将棋系の遊びの「先読み」が苦手な方でして。
読みが甘いというか、何かしら見落としてサクッと負ける感じです。

今回の実装では、その程度の「僕が勝てないCPU」にはなってくれたようなので、まあ満足しています。
とはいえまだまだ弱いなーとも。定石がしっかり頭に入ってる方には負けるかと思います。僕が遊んでても、稀に明らかな敗着手を選んでしまう様子? があるような感じでした。

強いCPUを作るのは本記事の主題ではありませんが、興味がある方は探索の効率化、枝刈り、評価済み局面のキャッシュ、定石評価といった手法を調べてみてくださいね。

negamax自体、今回は何も考えずに再起処理で実装していますが、JavaScriptで安易に再帰処理を行うと簡単にコールスタックを食い潰すのであまり良くないです。
今時のPCやスマホなら多少は再起処理に耐えてくれるようですが……max_depth 5だとなかなか応答が返ってこない感じですね。

計算中はUIスレッドをロックしてしまいます。この実装も良くないですね。
ちゃんと実装するなら、中間状態を保持できるようにして処理を定時分割して、計算進捗表示などを行えるようにすべきですね。
(改良検討中なのですが……バグって辛くなってきたので一旦端折っております:bow:

今後の課題ですね。精進します。

Rustで実装したCUI版について

**: a b c : Side.B captured
==:============ : 
:1:  🦒B🐘B : Side.A captured
:2:🦁B🐥B🦁A : 
:3:  🐥A   :
:4:🐘A  🦒A :

Side.A's turn. hands:4 

current turn: 5

**: a b c : Side.B captured
==:============ : 
:1:  🦒B🐘B : Side.A captured
:2:🦁B🐥A🦁A : 🐥
:3:       :
:4:🐘A  🦒A :

Side.B's turn. hands:2 

current turn: 6

Rustの学習と実装が今回一番時間がかかったところですね。未経験の言語ですのでまあ仕方がないところではありますが……入門の時点でなかなか手強い言語でした。

さて、CUI版の実装ですが、こちらはRust実装の挙動テスト目的の実装です。
cargo runして、enterキーを押すたびに一手ずつCPU同士が対戦を進めるコードを書きました。

同じプロジェクト上で、 cargo build --lib --target=wasm32-unknown-unknown --release; を実行するとwasmバイナリファイルが出来上がります。実作業時はcpコマンドと合わせてReactのプロジェクト側にデプロイ(?)まで一貫処理してます。なかなか理想的な作業イテレーションなのでは。cargo watchあたりも試したいですが今回は保留(なんかエラーになった(´・ω・`)

Rustの言葉でいうと、main.rsのバイナリクレートがCUI、lib.rsのライブラリクレートがwasmを出す構成、ということになりますか。シンプルに同居できるのはいいですね。

参考にしたサイトと、Rust学び始めの所感

学習にはrust-jpの公式ドキュメント和訳プロジェクトと、zennでmebiusbox様が公開されている入門ドキュメントが助けになりました。特にmebiusbox様のRust入門は初学者にざっくり概要を伝えてくれるとても良い入り口になりました。感謝です。

概要把握はmebiusbox様のドキュメントで、細かい詳細はrust-jpで追いかけていくのが良さそうです。
正直英語情報を追う覚悟もあったのですが、情報が十分だったのか結局最後までドキュメントで困ることはなかったように感じます。

Rustの特徴かつ最初の難関と紹介されがちな「borrow checker」と「所有権」ですが、やはり手を動かして実際に悩んだほうが理解が早いように感じました。
僕のイメージだと、序盤でつまづいたところの印象は所有権の難しさではありませんでした。
「これまで意識せず適当に書いてたコードがコンパイルエラーにされる」という説明の方が合ってるのかも。
「その書き方は良くないよ」と諭され続けている感覚も伴います。
結果として、コンパイルエラーに学びながら、自然に綺麗なコードになっていく感じもあります。

なんかこう、手厳しい先生に見守られてる感じですかね^^;

今回はRustの言語機能については深追いしていません。
RC/Box/Cell/RefCellなどは一切スルーしていますので、「Rustの入門の入門」あたりかなと思います。

色々と、理解不足に伴う挫折もありました。
途中でHashMapを使おうとして、構造体メンバーに追加すると……immutableなので変更できないという状態になり混乱しました。えーっていう(´・ω・`)
よくわからないままRefCellとか触り始めて実行時のBorrow Checker Panicを踏み続け、僕もパニックです(´・ω・`)
まあ僕にはまだ早かったということでしょう。

今回は一切HashMap使わない実装で片付けました。
もう少しレベル上げてから再挑戦します。次は倒す(´・ω・`)

実装周りも軽くご説明

さて、少し実装面についての説明も。

Rustでも盤面表示とルール評価を実装しているので……UI操作以外のほぼ全てについて、TypeScript版と同じ処理を二重実装している状態です。
無駄骨と感じられるかもしれません。ただ、まあ今回は学習目的ですので。
別言語で動いてるロジックがあれば比較しながら進められるので、安心感もありました。
意味のある二度手間だったと思います。

今回はwasm作成が目標なこともあり、スレッドも使わず、単純な計算ロジック以上のことを考えなくて済むのは結果として幸いだったかもしれません。

強いて工夫したと言えそうなのは……評価の各種処理段階をOptionで遅延評価にしたところくらい?
TypeScript版でごちゃごちゃしていたコードがスッキリしました。

こうなるとRust側コードを参考にTypeScript側ももう一度ゼロベースで描き直したい気持ちも……。
ま、今回は体力尽きたのでこの辺で(´・ω・`)

Rustの辛さと良さの所感など

やはり最初はBorrow Checkerさんには大量に怒られましたし、「えー、じゃあどう書けばいいんだよこれ」って投げ出しそうになることも多々。

ただ、基本的には「設計がまずい・考え方がまずい」時に怒られてる感じでしたかね。
考えすぎて無駄に複雑に書いちゃった時なんかにも、borrow checkerさんは見逃しません。
その度に「あ、これもっと単純に書けるよな?」と考え直して整理してました。
&mutが増えてきたら、コンパイルする前から「なんか間違った方向に進んでる気がするな」と自省するようにも。
この辺、もしかしてRustの短所ではなくて、むしろ長所なのかも。

ライフタイム指定子周りがまだよくわからないな……。文字列リテラルの型が「&'static str」というのはちょっと初見殺しな気がしました^^;

素直に「Rustいいな」って感じる点も多いです。

言語設計の方針はかなり好みですね。Null安全で型推論が強力、記述量を減らしつつ実行時エラーとも無縁に取り組める。コンパクトなコード量の割に、表現力が豊かな言語だと思います。
(ただ、fn/pub/implあたりは略しすぎな気もするかなぁ。。zigもfnだっけ。。今時は略しまくるのがトレンドなのかしら……?

テストも書きやすく、依存関係を記述一発で持ってこれるのもいいですね。
cargo testでユニットテストを一つ一つ潰していくイテレーションがとても良かったです。
正しいコードとテストを積み上げていく作業は心地よいですね。

rustupとcargoの使い勝手、追加パッケージの導入の手軽さは流石に今時の言語だなと思います。

Visual Studio Code機能拡張の「rust-analyzer」もいい仕事をしてくれました。定義ジャンプやquick fix、型推論結果の自動表示などが充実していて、コーディング自体のストレスはありません。

まだ基本機能しか使っていない段階ですが、borrow checkerさんに辛さを感じない程度には慣れました。
ま、最初の敷居は高いと感じましたし、学習を進めたらまだまだ壁があるとは思います。

まあ、もうちょい触ってみてからですね。
僕はまだまだ入門以前という感じです。

WASMについて

まずご存知ない方のために、ざっくりとWebAssemblyの説明を。
ご存知の方には釈迦に説法かと思います。適宜読み飛ばしていただければと:bow:

昨今のブラウザ上で何か独自の処理を行うには、プラグインを使うか、そうでなければJavaScriptしか選択肢がない状態です。
そんな中、wasmが2020年に公式なweb標準となりました

WebAssemblyは、JavaScriptの代替ではありません。JavaScriptと連携することで、コードサイズの最小化や速度効率など、各種の課題解決をもたらすことが期待されてます。

WebAPIを直接触れるものではなく。どうもJavaVMと比較されることも多いご様子ですが、目的がだいぶ違うのでそこも含めて話題になってる感じですかね。JavaVMは万能選手を意図して作られましたが、wasmは計算しかできません。

もちろんJavaScriptとの連携は最低限確保されてますし、計算処理効率やメモリ効率について効率化が見込める技術です。wasmファイル自体が小さく済むなどの利点もあります。

wasmはc/c++やRustその他の言語からコンパイルできるのも魅力ですね。
制限はいろいろありますが、過去の資産がwasmでブラウザ上に復活しているのもc/c++から出力できるという汎用性が貢献している様子ですね。old pcファンとしては応援せざるを得ない流れです

wasmからwebAPIを叩くことの悩みは、やはりJavaScriptを経由せざるを得なくて、コード最小化の恩恵も得にくく、工夫がなければ単なるオーバーヘッドが生じるだけに終わってしまうところです。

執筆時点の2023/1現在では、「wasmが役立つ用途」をちゃんと見据えて計画しないと空回りする可能性もありそうなので注意したいところです。

要点は二つあるようです。

  • (1) wasm側に十分計算負担を任せられること
  • (2) 頻繁にJavaScript側と通信しないこと

まず(1)を満たすタスクが思いつかない際は、wasm化する意義が薄いかもしれません。
この辺はプロファイリングを通して有益性を確認してから実務に落とし込みたいところですね。
「JavaScriptは遅い」という前提で話を進めてはいけません。結構速いんですよ、今のJavaScript。
TypeScriptからの最適化もあるかもしれませんし、Chrome/Safari/Firefoxが本気でブラウザのJavaScriptエンジンの高速化レースを続けているのも伊達ではないと思います。

wasmはwebとついてる割に、ブラウザ以外にも色々話題が尽きない技術です。話が長くなってしまいましたので、興味のある方はwikipediaあたりから各種情報を調べていただくのが良いかと。丸投げですみません。

Rustとwasm

Rustはこのwasmをサクッと出せる環境のひとつとしても好評なご様子ですね。
実際、いくつかチュートリアルを元に繋ぎ込み方を試したらあっさり動作確認できました。手軽すぎて拍子抜けするレベルです。
環境整えるのも、rustupでwasmをtarget追加しただけですね。簡単だなぁ……。

wasmを出せるプログラム言語は多数ありますので、いろいろ試していきたい気持ちです。

wasm実装周り - wasmにやらせる価値のある仕事を考える

今回作成したwasmでは、以下の仕事する公開メソッド「get_next_hand()」を切り出しました。

  • 盤面の状態、手駒情報、手番のサイド、negamaxのdepthを引数で受け取る
  • 次に打つ手の情報を返す

wasmは頻繁にブラウザ(JavaScript)とやりとりするとオーバーヘッドになりやすい技術です。
今回は話を簡単にするために、「negamaxくらいのまとまった仕事量を与えよう」と考えて実装しました。

ゲームオーバー判定などはTypeScript側の処理をそのまま使っています。
TypeScriptとのバインドには何もライブラリを使っていません。最小構成にこだわりすぎた感じもありますが、どこまで容量を小さくできるのかには興味があったのでなんとなくつい。

このため、wasmからJavaScript側に「どうやって手情報を返すか」については少し悩みました。
「どのコマをどこに移動させるか」、という表現には、to/fromのx/y座標セットが必要かなと考えました。この情報を返すには4要素が必要になりそうです。
ただ、素のwasmからJavaScriptに任意の情報を返すのは一手間必要そうな気配ですね。共有メモリの操作とかでいけそうなんですが、メモリ操作かー。うーん、って感じでなんとなく迂回。

今回は、ちょっと泥臭いですがビット演算で詰め込むことで対応しました。
どうぶつしょうぎ™の盤面は、4x3ととても小さいものです。座標情報もコマ種別も、全て4bitに収まります。それを4要素返せれば、今回は十分そうですね。
手駒を打つときは、最初のx座標に範囲外の「4」を入れるルールにしました。

では64bitの返り値変数に、ビット演算で詰め込んでみましょう。
こんなコードになりました。

    // rust側: ret1,2,3,4を4ビットごとに詰め込む
	return (ret1 + (ret2 << 4) + (ret3 << 8) + (ret4 << 12)) as f64;
    // typescript側: wasmの戻り値を4ビットごとに別の変数に分解して受け取る
    const ret1 = wasm_result & 0xF;
    const ret2 = (wasm_result >> 4) & 0xF;
    const ret3 = (wasm_result >> 8) & 0xF;
    const ret4 = (wasm_result >> 12) & 0xF;

……泥臭いな(´・ω・`;) あと後者は分割代入で書けた気がする
jsとrustの受け渡し周りはまだまだ勉強不足ですね。泥臭くないやり方を引き続き調査したい気持ちです。

wasmはサイズが小さいのも魅力ですね。get_next_hand()に必要なメソッド群は34kbでした。wat見るとpanic時の文言などもくっついちゃってるようなので、この辺追いかけたらもう少しコンパクトにできるかも?

wasmバイナリの実行時デバッグの方法はちょっと調べておかないといけないなー……。まあテストを整えたので最終的にあまり悩まずに動くものになったと思います。

今回は最小構成を把握したかったためスルーしましたが、web-sysやbing-gen、yewなどの踏み込んだRust/wasmの世界にも触れていきたいですね。

所感まとめ

新しいプログラム言語を覚えるのは楽しいですね。
Rustは覚えておいて損のなさそうな言語なので、引き続き学んでいきたいです。
wasmが活躍できそうな状況についてはまだイメージが掴めず。
ゲームのCPU作るのには悪くなさそうですが、もっと何かあるよねぇ……。
c/c++の資産を移植する方面についても、もう少し調べてみたい気持ちです。

あとこの記事の推敲中に、「wasm書くならzigがいいぞ」と勧められて目移りしています。
また新言語ですか……体力持たないよ。またこれも面白そうなのが困る(´・ω・`;)

商標について

どうぶつしょうぎ™は北尾まどか様の商標です。ご留意くださいませ。
https://www.j-platpat.inpit.go.jp/c1800/TR/JP-2009-032168/F014B140A29B4421B8334F8951D1E478AEDC9619C6840E1AB19C6FCEAE1FF431/40/ja

4
1
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1