React+TypeScriptの勉強がてらオセロを作成してみたら案外苦戦したので、概要とReact特有の難しかった点を書いておきます。
- ステップバイステップで作成してみよう!みたいな内容ではありません(需要があったら作ります)
- デザイン性は皆無です。あくまでロジックに焦点を当てて作成しました。
- 初学者ゆえ、もし不備がありましたら遠慮なくお申し付けください。
成果物
Github Pages (実際にプレイできます)
https://yudatchshouga.github.io/react-test/
Github (ソースコード)
https://github.com/yudatchshouga/react-test
制作期間:2日
概要
オセロのルール周りに関しては省略します。
- 盤面は4*4である(コード上で簡単に変更可能)
- 盤面には現在のプレイヤーが駒を置ける場所をハイライト表示する
- 盤面の上部にはゲームステータスが表示される
- プレイヤーが駒を置ける場合、
現在のプレイヤー: ⚫︎
のように表示 - 片方のプレイヤーが駒を置けなくなったら
パス
と表示 - 両者駒を置けなくなったら
ゲームオーバー
と表示
- プレイヤーが駒を置ける場合、
- パスが発生した場合、1秒間ステータスに
パス
と表示されターン交代する - CPUは今回は実装していない(チャレンジ予定)
余談ですが、ChatGPTに聞いたところ、駒を置ける箇所の精査にはどうしても$O(N^3)$かかってしまうらしいです($N$は盤面の一辺のサイズ)。
React特有の難しかった点
状態管理
最初はGameという変数に色々な状態を集約して、Gameを1つの状態として扱ってました。ですが、様々な判定を加えていくにつれて再レンダリングが何重にも発生してしまうという事態になったので、状態を分離しました。最終的には以下の状態を採用することにしました。
// 盤面
const [board, setBoard] = useState<string[][]>(initBoard);
// 盤面に駒が置けるかどうかのフラグ
const [putables, setPutables] = useState<boolean[][]>(initPutables);
// プレイヤー
const [player, setPlayer] = useState<string>(initPlayer);
// ステータス
const [gameStatus, setGameStatus] = useState<string>("playing");
なお、ステータスは以下のように定義しました。
- 駒を置ける状態 →
playing
- 現在のプレイヤーが駒を置けず、次のプレイヤーが駒を置ける状態 →
pass
- 両者が駒を置けない状態 →
gameover
状態更新とuseEffect
について
ReactではuseState
のsetXxx
で状態更新をします。
単純な更新ならいいのですが、オセロのようにロジックが複雑になってくると、
-
board
の状態を更新(駒をひっくり返す) -
player
を更新(ターン交代) -
board,player
を使ってputables
の更新(置ける場所の確認) -
putables
を使ってステータスを更新(pass, gameover
の判定)
のように更新された状態を用いて別の状態を更新する処理が多発します。
この時、愚直に1,2,3,4と処理を順番に書くと、2でplayer
の更新が終わっていないのに3の処理に進んでしまうというバグが起きてしまいます。
どうやらReactの状態更新処理は非同期処理であることに起因しているようです。
これらの問題を解決してくれるのがuseEffect
です。
先ほどの2→3の処理が順番に行われない問題は以下のように書くことで解決します。
// プレイヤー交代したら置ける場所を更新する
useEffect(() => {
// 置ける場所を探す
setPutables(() => calculatePutables(board, player));
}, [player]);
このコードでは、player
を監視対象に設定し、player
が変更されたらuseEffect
の中身が実行されます。
このようにuseEffect
をうまく利用することで、非同期処理である状態更新の順序をうまく制御することができます。
useEffect
は、パスの際に1秒待機する処理を書く際にも活躍してくれました。
最後に
今回はオセロをまる2日ほどかけて作成してみました。最終的にはCPUやAIを実装したいので、ロジックはバックエンドに移すかもしれませんが、とりあえずReactオンリーで書きました。
あと、本当はカッコつけて色んなクラスを作成して作っていたのですが、基礎ができていないうちにやるもんじゃないなと思いました。(自戒)
最近X始めたので、フォローいただけると嬉しいです!
https://twitter.com/yudatchshouga