Closed12

Reactで作るオセロ 備忘録

ebinaebina

経緯

プログラミングをする中で何かとオセロをテーマにしてきた

このスクラップではLaravelによるDDDベースのバックエンドの存在を前提として、リッチなUIを作成するためにReactを導入にチャレンジ
https://zenn.dev/ebiiina/scraps/05612034d5774c
https://zenn.dev/ebiiina/scraps/2c1d4f6b4f694d

今回は?

クライアント側に全てのロジックをもたせたSPAとしてのオセロをReactで実装してみる

ebinaebina

これまでに作ったもの

https://othello.ebinas.dev/
開発したオセロのゲーム画面

GitHub
https://github.com/ebinase/othello-frontend

現状まとめ

🙆‍♂️

  • Next.js 12を使用したSPA
  • 一度読み込めばオフラインでもプレイ可能
  • デザインはニューモーフィズム(風)

🙅‍♂️

  • 先行/後攻、対戦相手の選択不可
    • 行動順や対戦相手をハードコーディングしてしまっているため
  • アーキテクチャやディレクトリ構成が整理できていない
  • Botの種類
ebinaebina

ここまでの流れ

まずはオセロが動くことを目指した

インストールは以前の記事を参考にした
https://zenn.dev/ebiiina/scraps/05612034d5774c

オセロ成り立たせる構成要素はこんな感じ

  1. 盤面UI
  2. 盤面・ターン更新ロジック
  3. UIと処理を紐づける仕組み

reactで作る場合は

  • 1と2と3をまとめてコンポーネント内に書く
  • 2をカスタムフックに切り出し、1のコンポーネント内で呼び出す(3

最初は前者でやっていたが、コンポーネントの肥大化していったため後者のスタイルに移行。

オセロはターンに対して、更新・スキップといった複数のアクションを取り得るので内部的にuseReducerを使用し、useOthelloというカスタムフックを作成した。

使い方は

const [state, dispatch] = useOthello();

// スキップしたいとき
dispatch({ type: "skip" });

// ボットの計算結果を反映したいとき
const result = someBotFunction(state.board, state.color); // 盤面と石の色を受け取って指したい場所を返す
dispatch({ type: "update", fieldId: result })

// プレイヤーがフィールドをクリックしたとき
<Field onClick={() => dispatch({ type: "update", fieldId: props.fieldId })}>

これでいったん動作はするようになった

ebinaebina

課題感

Next.js 13へのアップデート

並行して開発していたチャットアプリで13を使用したこともあり、こちらも揃えていきたい
(AppDirやSuspenseベースへの移行など最新のトレンドのキャッチアップもしていきたい)
https://github.com/ebinase/hana-chat-front

対戦設定機能追加

現状で対戦相手だけでなく先行後攻すら選べないのはさすがにまずいので機能追加を行う

リファクタリング

ファイルの命名規則やディレクトリ構成がメチャクチャなので直していく。

もともとはこちらを参考にしていましたが自分の理解不足でカオスに...
https://zenn.dev/yoshiko/articles/99f8047555f700

こちらの記事のデザインパターンがしっくり来たので、こちらをベースに直していく
https://zenn.dev/mutex_inc/articles/beca85dd7fdcae

やりたいこと

エージェントアーキテクチャの導入

現在のコードには明確な設計意図がなく、プレイヤー情報やゲームの状態管理、Botの導入を行っていく上で変更容易性に不安が残る。
そこで純粋なオセロの世界と、ゲームプレイの流れをマネジメントするエージェントを分離するエージェントアーキテクチャを導入する

丁度いい歯ごたえのBot作成

現在のBotはランダムな手を返すものと、初歩的なモンテカルロ木探索を実装したものの2種類しかなく強さが両極端になっている。
そこでビヘイビアツリーの導入なども行いながら適切なレベル感のBotの作成やベンチマークによる強さの定量化も行っていきたい

ebinaebina

Next.jsのアップグレード

12→13へアップグレードする。

  • appDirなどによって既存コードが大きく影響を受けるため、早めに対応しておく
  • 合わせて使用していないライブラリの削除も行う

公式のアップグレードガイド(新機能向けのdoc)

https://beta.nextjs.org/docs/upgrade-guide

関連するライブラリのアップデート

npm install next@latest react@latest react-dom@latest @types/react@latest @types/react-dom@latest eslint-config-next@latest eslint-config-prettier@latest

npm run devを実行してエラーなく動くことを確認
(この時点ではappDirは有効化していない)

新機能の有効化をしていく

https://beta.nextjs.org/docs/upgrade-guide#migrating-from-pages-to-app

appDirの有効化

/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
+  swcMinify: true,
+  experimental: {
+    appDir: true,
 + },
};

module.exports = nextConfig;

ここで試しに開発サーバーを立ち上げたところ自動でtsconfigの設定を修正してくれた

$ npm run dev

info  - Thank you for testing `appDir` please leave your feedback at https://nextjs.link/app-feedback
We detected TypeScript in your project and reconfigured your tsconfig.json file for you. Strict-mode is set to false by default.

The following suggested values were added to your tsconfig.json. These values can be changed to fit your project's needs:

        - include was updated to add '.next/types/**/*.ts'
        - plugins was updated to add { name: 'next' }

info  - VS Code settings.json has been updated for Next.js' automatic app types, this file can be added to .gitignore if desired

またnext-env.d.tsに追加された行はNextのIssueで言及があった

/// <reference types="next" />
/// <reference types="next/image-types/global" />
+ /// <reference types="next/navigation-types/compat/navigation" />

https://github.com/vercel/next.js/issues/46360

ページファイルの移動

以上でコード側の対応は完了。
最終形はこちら
https://github.com/ebinase/othello-frontend/pull/19/files#diff-eca96d2c09f31517696a26e1d0be4070e1fbab02831481bed006e275741d030b

Vercelのnpmバージョン修正

Vercelへのデプロイ時にbuildに失敗するようになってしまったため、buildやapi routesに使用するnpmランタイムを18.xに変更

エラー

SyntaxError: Unexpected token '||='
    at wrapSafe (internal/modules/cjs/loader.js:1029:16)
    at Module._compile (internal/modules/cjs/loader.js:1078:27)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1143:10)
    at Module.load (internal/modules/cjs/loader.js:979:32)
    at Function.Module._load (internal/modules/cjs/loader.js:819:12)
    at Module.require (internal/modules/cjs/loader.js:1003:19)
    at require (internal/modules/cjs/helpers.js:107:18)
    at Object.<anonymous> (/vercel/path0/node_modules/next/dist/build/entries.js:59:16)
    at Module._compile (internal/modules/cjs/loader.js:1114:14)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1143:10)
Error: Command "npm run build" exited with 1
Deployment completed
BUILD_UTILS_SPAWN_1: Command "npm run build" exited with 1

バージョン変更

なお、Next.js13のnpmの要件はver16.8以降となっている

The minimum Node.js version is now v16.8. See the Node.js documentation for more information.
引用元:https://beta.nextjs.org/docs/upgrade-guide#nodejs-version

ebinaebina

補足

browserslistのアップデート

npm run dev実行時、以下のメッセージが出たのでアップデートする

$ npm run dev

# 略...
Browserslist: caniuse-lite is outdated. Please run:
  npx browserslist@latest --update-db
  Why you should do it regularly: https://github.com/browserslist/browserslist#browsers-data-updating

https://dev.classmethod.jp/articles/asked-to-update-the-browserslist-when-building-react-app/

$ npx browserslist@latest --update-db
Need to install the following packages:
  browserslist@latest
Ok to proceed? (y) #y

# インストール成功

無事エラーが発生しなくなった

$ npm run dev

> othello-frontend@0.1.0 dev
> next dev

ready - started server on 0.0.0.0:3000, url: http://localhost:3000
ebinaebina

デザイン適用

TailwindCSSを使用する

デザイン案

こちらをもとにデザインを当てていく

結果

https://github.com/ebinase/othello-frontend/pull/21

Botターン

試合結果

感想

総評

全体的にレスポンシブ+リキッドレイアウトで比較的きれいにまとめることができた。
アーキテクチャの都合で暫定的な実装をした所も多いのでエージェントアーキテクチャの実装を進めたい。
また、アクセシビリティ(キーボード操作など)にも対応してきたい。

Tree Design

  • DOMの構造とディレクトリが一致していて分かりやすかった
    • 新たにコンポーネントを作る際に置き場に迷わないのはストレスフリーだった
  • ルートコンポーネントがindex.tsxだとエディタで開いたときに見失いやすいと感じた
    • 「ディレクトリ名と同じ名前のファイルはルート」というルールにしてみる

Tailwind

  • いちいちクラス名を命名をしなくていいのでとても楽
  • ただし、個人で集中的に開発をしているからこそなんとかなっている側面が大きい
    • 複数人で開発する場合はスタイルやコーディングの一貫性を保つのが難しそう
    • ユーティリティクラスを列挙するだけなので、構造化や名前付けをできずスタイルの設計意図を伝えにくい
      • これは名前をつけなくて済む、ことのトレードオフ
  • Tailwindだけでは表現しきれずCSSを直書きすることもままある(独自スタイルを登録する手もあるが...)
  • 調べるコストがまあまあある
    • 「CSSではこう書くけど、Tailwindではどう書けば良いんだ?」という事態に遭遇する
    • 直接CSSを書いたほうが早そう、となりがち
  • 総じて保守性に課題がありそう
    • tailwind.config.jsで共通スタイルをまとめていけるのでここをフル活用すればなんとかなる?
    • そもそもユーティリティファースト自体が「まずはスタイルを当ててあとから共通化」という思想なので正しい経路をたどっているのかも
    • このあたりのノウハウを集めていきたい
ebinaebina

タスク管理

Next

プロジェクト全体にTree Design(一部カスタム)を適用しつつ、不要なファイルの削除やコードのフォーマットを行う
その後、エージェントアーキテクチャの実装へ

ebinaebina

盤面周りのロジックの整理

課題

  • 盤面の分析や更新のロジックが散らばっている
  • 複数箇所で何度も同じ計算が行われている
  • useOthelloフック内に盤面のロジックとゲームルールのロジックが混在している

雑感

  • 一つの盤面の状態(ターンやゲーム参加者等の情報は除く)に対して分析系の関数の結果は同じ
    • であればひとつの盤面に関する情報をイミュータブルな構造体にまとめれば良さそう
    • 計算結果はキャッシュして持っておく
  • 盤面はゲームの状態管理とは別に独立したコンポーネントとして切り出す
    • useState()のように盤面の状態とその更新関数のみ公開する
  • ゲームの勝敗判定などのルールはエージェントに知識を持たせる

このあたりは気を抜くとオブジェクト指向の考え方が顔を出してしまうが、最低限に留めておきたい。
他言語における構造体やデータ志向アーキテクチャ、React流の関数型プログラミングなどのパラダイムの整理ができていないのでこれを気に整理していきたい

ebinaebina

間が空いてしまった

ざっくり

  • グローバルステートを管理する方法を模索
  • Recoilを導入→zustandに乗り換える

Recoil

調査時に理解を助けられた記事

Recoil導入のモチベと概念

ProviderタワーをRecoilに置き換える

  • たしかにProviderタワー化していくのしんどそう
  • グローバルステートをカスタムフックでどこからでも読めるのば便利そう
  • atomとselectorがあるのね

ステート管理を超えるRecoil運用の考え方

  • 図でめっちゃ理解しやすい
  • データグラフによって依存関係を整理できる
  • recoilがデータとロジックに責務を持ち、その結果をreactが画面表示を行うという責務の分離がとても良いなぁと感じた
    • reactの**「UIライブラリ」への回帰**
  • fluxを置き換えれるなら今回のオセロもこの戦略で行けそう(ほぼfluxだった)

Recoilにロジックを載せる運用戦略

  • より詳細な戦略
  • 完全には理解できていないがデータフロー(依存関係)をベースにロジックも組んでいけそう
  • 非同期処理も載せていける
  • となるとキーの管理と依存関係の見える化が運用していく上での課題になりそう

実際に学習をしていく手順

Reactの状態管理ライブラリ「Recoil」について - Qiita

  • 実際の導入手順や具体的なコードが多く参考にしやすかった
  • フォルダ構成とかもありがたい

Recoil について勉強した

  • recoil登場当時の記事だけどまとまっていてわかりやすかった

Recoil導入方針

  • まずは入れてみる
  • カスタムフック経由で利用する前提で使う(recoilは露出させない)
  • 最初はグローバルステートを扱う、ことだけに利用する
  • データフローグラフの整備は後から
  • atom, selectorの粒度
    • まずは細かく分けすぎずにゲーム状態や参加者、盤面等の荒い粒度で切る
    • 盤面の各種計算パラメータはいったんオブジェクトの形で盤面atomで保持
      • 後でselectorに切り出しても良いかも

https://github.com/ebinase/othello-frontend/pull/24

更新系処理

オセロの更新系(Write)の実装をRecoilのatomとselectorに当てはめることに苦戦

https://zenn.dev/koushisa/scraps/2ebedc2d009e1c

  • Recoil/JotaiはRead系、Redux, zustandはWrite系と相性が良い

https://zenn.dev/hrbrain/articles/437d0b7492ac47#stateはどのように管理する?

  • Stateが頻繁に更新される場合
    その場合はRecoilやJotaiといったatom-basedなライブラリを採用するのがいいでしょう。
  • 異なる多くのStateをComponent間で共有する必要がある場合
    その場合は、ReduxやRedux Toolkitがいいでしょう。
  • 上記のどちらでもない場合
    もしも多くのStateをComponent間で共有する必要性やStateの頻繁な更新がない場合、ZustandやReact Contextを採用するのがいいでしょう。

結論

zustandを導入してみる

改めて整理すると

  • 検索機能のようなユーザー操作に応じて細かくfetchするような処理は無い
  • ユーザーアクションをトリガーにしてステート全体を管理したい
  • ゲーム全体の状態の整合性を重視する
  • Reduxほど重くなく簡単に書けると嬉しい
  • もともとの自分の実装ではuseReducerを使っていた
ebinaebina

Material Tailwind

Tailwind CSSで実装したMaterial Design
https://www.material-tailwind.com/

背景

UIの改善を行っていく上で、一般的なパーツは自分でtailwindを使って書くよりもUIコンポーネントライブラリのパーツを使ったほうが早いのでは?

統一感のあるきれいなUIを素早く実装するためマテリアルデザインを導入してみる
すでにTailwind CSSを利用していたため、Material Tailwindを採用

導入取りやめ

いったん導入までしてみたが

  • Materialデザインに準拠したデザインへの移行が大変
    • tailwindベースのスタイリングをしてしまっている(一応カスタムテーマで対応もできる)
  • 他のUIライブラリとの比較検討が十分に行えていない
  • そもそもUIコンポーネントでデザインを統一する理由が薄い
    • シングルページのシンプルなアプリ
    • オセロゲームということもあり独自パーツが多い

以上からまだ必要な段階ではないと判断。
もう少し機能実装を進めて(拡張性や保守性で)しんどくなったらその問題に応じて必要な打ち手を考える

構成済みのコンポーネントが欲しいならChakra UIも良さそうだが、既存実装がtailwindのゴリ押しコードなので乗り換えるとしてもまた後でということになりそう

ebinaebina

更新メモ

少しずつリファクタを進めたのでメモ。時間があったらどこかにまとめる。

ドメインモデルでビジネスロジックを表現する

zustand → Jotai への移行

  • https://github.com/ebinase/othello-frontend/pull/53
  • 一つの巨大なストアに対して、同期的に更新処理とUI用のデータ整形を行うことでコードが肥大化し難読化していた
  • Jotaiを導入し、状態の更新とそこからリアクティブに表示用データを整形するデータフローグラフを構築した
    • 詳細はPRを参照
  • 自然と関心の分離が行われ、Jotaiの型システムも相まって良い開発体験ができた

データフローグラフ

描画の最適化

このスクラップは2024/04/05にクローズされました