Reactで作るオセロ 備忘録
経緯
プログラミングをする中で何かとオセロをテーマにしてきた
このスクラップではLaravelによるDDDベースのバックエンドの存在を前提として、リッチなUIを作成するためにReactを導入にチャレンジ
今回は?
クライアント側に全てのロジックをもたせたSPAとしてのオセロをReactで実装してみる
これまでに作ったもの
GitHub
現状まとめ
🙆♂️
- Next.js 12を使用したSPA
- 一度読み込めばオフラインでもプレイ可能
- デザインはニューモーフィズム(風)
🙅♂️
- 先行/後攻、対戦相手の選択不可
- 行動順や対戦相手をハードコーディングしてしまっているため
- アーキテクチャやディレクトリ構成が整理できていない
- Botの種類
ここまでの流れ
まずはオセロが動くことを目指した
インストールは以前の記事を参考にした
オセロ成り立たせる構成要素はこんな感じ
- 盤面UI
- 盤面・ターン更新ロジック
- 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 })}>
これでいったん動作はするようになった
課題感
Next.js 13へのアップデート
並行して開発していたチャットアプリで13を使用したこともあり、こちらも揃えていきたい
(AppDirやSuspenseベースへの移行など最新のトレンドのキャッチアップもしていきたい)
対戦設定機能追加
現状で対戦相手だけでなく先行後攻すら選べないのはさすがにまずいので機能追加を行う
リファクタリング
ファイルの命名規則やディレクトリ構成がメチャクチャなので直していく。
もともとはこちらを参考にしていましたが自分の理解不足でカオスに...
こちらの記事のデザインパターンがしっくり来たので、こちらをベースに直していく
やりたいこと
エージェントアーキテクチャの導入
現在のコードには明確な設計意図がなく、プレイヤー情報やゲームの状態管理、Botの導入を行っていく上で変更容易性に不安が残る。
そこで純粋なオセロの世界と、ゲームプレイの流れをマネジメントするエージェントを分離するエージェントアーキテクチャを導入する
丁度いい歯ごたえのBot作成
現在のBotはランダムな手を返すものと、初歩的なモンテカルロ木探索を実装したものの2種類しかなく強さが両極端になっている。
そこでビヘイビアツリーの導入なども行いながら適切なレベル感のBotの作成やベンチマークによる強さの定量化も行っていきたい
Next.jsのアップグレード
12→13へアップグレードする。
- appDirなどによって既存コードが大きく影響を受けるため、早めに対応しておく
- 合わせて使用していないライブラリの削除も行う
公式のアップグレードガイド(新機能向けのdoc)
関連するライブラリのアップデート
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は有効化していない)
新機能の有効化をしていく
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" />
ページファイルの移動
-
src/pages
からapp/
に移行-
_app.tsx
,index.tsx
→ 'page.tsx,
layout.tsx`の移行 - ページタイトルやdescriptionはmetadataとしてlayoutに定義
- faviconは自動で読み込まれるため設定不要
-
-
<Playground />
をClientSideコンポーネント化 - tailwindも
app/
に対応させた
以上でコード側の対応は完了。
最終形はこちら
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
補足
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
$ 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
デザイン適用
TailwindCSSを使用する
デザイン案
こちらをもとにデザインを当てていく
結果
Botターン
試合結果
感想
総評
全体的にレスポンシブ+リキッドレイアウトで比較的きれいにまとめることができた。
アーキテクチャの都合で暫定的な実装をした所も多いのでエージェントアーキテクチャの実装を進めたい。
また、アクセシビリティ(キーボード操作など)にも対応してきたい。
Tree Design
- DOMの構造とディレクトリが一致していて分かりやすかった
- 新たにコンポーネントを作る際に置き場に迷わないのはストレスフリーだった
- ルートコンポーネントが
index.tsx
だとエディタで開いたときに見失いやすいと感じた- 「ディレクトリ名と同じ名前のファイルはルート」というルールにしてみる
Tailwind
- いちいちクラス名を命名をしなくていいのでとても楽
- ただし、個人で集中的に開発をしているからこそなんとかなっている側面が大きい
- 複数人で開発する場合はスタイルやコーディングの一貫性を保つのが難しそう
- ユーティリティクラスを列挙するだけなので、構造化や名前付けをできずスタイルの設計意図を伝えにくい
- これは名前をつけなくて済む、ことのトレードオフ
- Tailwindだけでは表現しきれずCSSを直書きすることもままある(独自スタイルを登録する手もあるが...)
- 調べるコストがまあまあある
- 「CSSではこう書くけど、Tailwindではどう書けば良いんだ?」という事態に遭遇する
- 直接CSSを書いたほうが早そう、となりがち
- 総じて保守性に課題がありそう
-
tailwind.config.js
で共通スタイルをまとめていけるのでここをフル活用すればなんとかなる? - そもそもユーティリティファースト自体が「まずはスタイルを当ててあとから共通化」という思想なので正しい経路をたどっているのかも
- このあたりのノウハウを集めていきたい
-
タスク管理
- カンプランを採用
- https://www.atlassian.com/ja/agile/kanban/kanplan
- 細かなTODOをバックログに貯めていき、実際に作業するものはカンバンで管理する
- Notionのボードビューを使用
Next
プロジェクト全体にTree Design(一部カスタム)を適用しつつ、不要なファイルの削除やコードのフォーマットを行う
その後、エージェントアーキテクチャの実装へ
盤面周りのロジックの整理
課題
- 盤面の分析や更新のロジックが散らばっている
- 複数箇所で何度も同じ計算が行われている
- useOthelloフック内に盤面のロジックとゲームルールのロジックが混在している
雑感
- 一つの盤面の状態(ターンやゲーム参加者等の情報は除く)に対して分析系の関数の結果は同じ
- であればひとつの盤面に関する情報をイミュータブルな構造体にまとめれば良さそう
- 計算結果はキャッシュして持っておく
- 盤面はゲームの状態管理とは別に独立したコンポーネントとして切り出す
- useState()のように盤面の状態とその更新関数のみ公開する
- ゲームの勝敗判定などのルールはエージェントに知識を持たせる
このあたりは気を抜くとオブジェクト指向の考え方が顔を出してしまうが、最低限に留めておきたい。
他言語における構造体やデータ志向アーキテクチャ、React流の関数型プログラミングなどのパラダイムの整理ができていないのでこれを気に整理していきたい
間が空いてしまった
ざっくり
- グローバルステートを管理する方法を模索
- Recoilを導入→zustandに乗り換える
Recoil
調査時に理解を助けられた記事
Recoil導入のモチベと概念
- たしかにProviderタワー化していくのしんどそう
- グローバルステートをカスタムフックでどこからでも読めるのば便利そう
- atomとselectorがあるのね
- 図でめっちゃ理解しやすい
- データグラフによって依存関係を整理できる
- recoilがデータとロジックに責務を持ち、その結果をreactが画面表示を行うという責務の分離がとても良いなぁと感じた
- reactの**「UIライブラリ」への回帰**
- fluxを置き換えれるなら今回のオセロもこの戦略で行けそう(ほぼfluxだった)
- より詳細な戦略
- 完全には理解できていないがデータフロー(依存関係)をベースにロジックも組んでいけそう
- 非同期処理も載せていける
- となるとキーの管理と依存関係の見える化が運用していく上での課題になりそう
実際に学習をしていく手順
Reactの状態管理ライブラリ「Recoil」について - Qiita
- 実際の導入手順や具体的なコードが多く参考にしやすかった
- フォルダ構成とかもありがたい
- recoil登場当時の記事だけどまとまっていてわかりやすかった
Recoil導入方針
- まずは入れてみる
- カスタムフック経由で利用する前提で使う(recoilは露出させない)
- 最初はグローバルステートを扱う、ことだけに利用する
- データフローグラフの整備は後から
- atom, selectorの粒度
- まずは細かく分けすぎずにゲーム状態や参加者、盤面等の荒い粒度で切る
- 盤面の各種計算パラメータはいったんオブジェクトの形で盤面atomで保持
- 後でselectorに切り出しても良いかも
更新系処理
オセロの更新系(Write)の実装をRecoilのatomとselectorに当てはめることに苦戦
- Recoil/JotaiはRead系、Redux, zustandはWrite系と相性が良い
- Stateが頻繁に更新される場合
その場合はRecoilやJotaiといったatom-basedなライブラリを採用するのがいいでしょう。- 異なる多くのStateをComponent間で共有する必要がある場合
その場合は、ReduxやRedux Toolkitがいいでしょう。- 上記のどちらでもない場合
もしも多くのStateをComponent間で共有する必要性やStateの頻繁な更新がない場合、ZustandやReact Contextを採用するのがいいでしょう。
結論
zustandを導入してみる
改めて整理すると
- 検索機能のようなユーザー操作に応じて細かくfetchするような処理は無い
- ユーザーアクションをトリガーにしてステート全体を管理したい
- ゲーム全体の状態の整合性を重視する
- Reduxほど重くなく簡単に書けると嬉しい
- もともとの自分の実装ではuseReducerを使っていた
Material Tailwind
Tailwind CSSで実装したMaterial Design
背景
UIの改善を行っていく上で、一般的なパーツは自分でtailwindを使って書くよりもUIコンポーネントライブラリのパーツを使ったほうが早いのでは?
↓
統一感のあるきれいなUIを素早く実装するためマテリアルデザインを導入してみる
すでにTailwind CSSを利用していたため、Material Tailwindを採用
導入取りやめ
いったん導入までしてみたが
- Materialデザインに準拠したデザインへの移行が大変
- tailwindベースのスタイリングをしてしまっている(一応カスタムテーマで対応もできる)
- 他のUIライブラリとの比較検討が十分に行えていない
- Tailwindとの相性ならTailwind UI。でも高い。。。
- このあたりも参考にしたが精査しきれていない
- そもそもUIコンポーネントでデザインを統一する理由が薄い
- シングルページのシンプルなアプリ
- オセロゲームということもあり独自パーツが多い
以上からまだ必要な段階ではないと判断。
もう少し機能実装を進めて(拡張性や保守性で)しんどくなったらその問題に応じて必要な打ち手を考える
構成済みのコンポーネントが欲しいならChakra UIも良さそうだが、既存実装がtailwindのゴリ押しコードなので乗り換えるとしてもまた後でということになりそう
更新メモ
少しずつリファクタを進めたのでメモ。時間があったらどこかにまとめる。
ドメインモデルでビジネスロジックを表現する
- https://github.com/ebinase/othello-frontend/pull/37 + 後続のPR数件
- 特に更新系の処理の凝集度を高めた
- ライフサイクルは store → モデル生成 → 更新系処理 -> モデルをvalueに変換 -> store
zustand → Jotai への移行
- https://github.com/ebinase/othello-frontend/pull/53
- 一つの巨大なストアに対して、同期的に更新処理とUI用のデータ整形を行うことでコードが肥大化し難読化していた
- Jotaiを導入し、状態の更新とそこからリアクティブに表示用データを整形するデータフローグラフを構築した
- 詳細はPRを参照
- 自然と関心の分離が行われ、Jotaiの型システムも相まって良い開発体験ができた
描画の最適化
- https://github.com/ebinase/othello-frontend/pull/54
- 盤面の不要な再描画を防ぐ微修正