オーラス麻雀ゲーム開発日記構想から現在までの開発記録を、公開順にまとめています。

前回までの進捗
前回は、Vitestを使って順位計算やロン・ツモ・流局などのテストを追加した。
ここまでで機能自体はかなり増えてきたものの、そのぶんReact側で管理する値や処理も増えてきた。
点数、自分の席、目標順位、本場、供託、終局方法、和了者、放銃者……と、useStateだけでもかなりの数になる。
さらに、
- 入力内容の自動保存
- 条件のランダム生成
- 入力内容のリセット
- 必要なロン・ツモ点の計算
- 終局後の点数計算
- 順位計算
- 目標達成判定
などもある。
これらを全部App.tsxへ書いていくと、画面を組み立てるコードと計算処理が混ざってかなり読みにくくなりそう。
そこで今回は、Reactのカスタムフックを使って処理を整理した。
カスタムフックとは
Reactでは、useStateやuseEffectなどを組み合わせた処理を、自分で関数としてまとめることができる。
こうした関数をカスタムフックと呼ぶ。・・・らしい。
このあたりは初見のため、またどこかで知識整理もかねて記事にしたいところ。
名前はuseから始める。
今回作っているのは主に次の2つ。
useGameState
useGameDerivedState
ざっくり分けると、
useGameState
→ 入力値や変更処理を管理する
useGameDerivedState
→ 入力値から計算できる結果を管理する
という役割にした。
入力状態はuseGameStateへまとめる
まず、ユーザーが入力した値はuseGameStateへまとめた。
たとえば、
const [scores, setScores] =
useState<Scores>(...);
const [selfSeat, setSelfSeat] =
useState<Seat>(...);
const [goal, setGoal] =
useState<Goal>(...);
const [honba, setHonba] =
useState<number>(...);
といった状態。
ほかにも、
- 難易度
- 供託
- 終局方法
- 和了者
- 放銃者
- ロン点
- ツモ時の支払い
- テンパイ者
などをここで管理する。
画面から変更できる値は、基本的にこちらへ集めた。
保存処理もuseGameStateで行う
前に追加したlocalStorageへの自動保存も、このカスタムフックの中にある。
useEffect(() => {
const stateToSave: SavedGameState = {
scores,
selfSeat,
goal,
difficulty,
honba,
kyotaku,
resultType,
winner,
loser,
ronPoints,
dealerPayment,
childPayment,
tenpaiSeats,
};
saveGameState(stateToSave);
}, [
scores,
selfSeat,
goal,
difficulty,
honba,
kyotaku,
resultType,
winner,
loser,
ronPoints,
dealerPayment,
childPayment,
tenpaiSeats,
]);
入力値のどれかが変わると保存される。
保存データの読み込みもuseGameState側で行うため、App.tsx側では保存処理を意識しなくてよくなった。
ランダム生成やリセットもまとめる
条件のランダム生成も、状態を変更する処理なのでuseGameStateへ入れた。
function randomizeCondition(): void {
const generatedCondition =
generateCondition(difficulty);
if (generatedCondition === null) {
return;
}
setScores(generatedCondition.scores);
setSelfSeat(generatedCondition.selfSeat);
setGoal(generatedCondition.goal);
setHonba(generatedCondition.honba);
setKyotaku(generatedCondition.kyotaku);
}
初期状態へ戻す処理も同じ。
function resetSavedState(): void {
clearSavedGameState();
setScores({
...initialGameState.scores,
});
setSelfSeat(initialGameState.selfSeat);
setGoal(initialGameState.goal);
// ほかの入力値も初期化
}
どちらも複数のuseStateをまとめて変更するため、画面側へ直接書くより一か所にまとめた方が分かりやすい。
計算結果はuseGameDerivedStateへ分ける
一方、ユーザーが直接入力するわけではなく、入力値から計算できるものはuseGameDerivedStateへまとめた。
たとえば終局後の点数。
const finalScores = useMemo(() => {
if (resultType === "ron") {
return applyRon(
scores,
winner,
loser,
ronPoints,
honba,
kyotaku,
);
}
if (resultType === "tsumo") {
return applyTsumo(
scores,
winner,
dealerPayment,
childPayment,
honba,
kyotaku,
);
}
return applyDraw(
scores,
tenpaiSeats,
);
}, [
scores,
resultType,
winner,
loser,
ronPoints,
dealerPayment,
childPayment,
honba,
kyotaku,
tenpaiSeats,
]);
入力した終局方法によって処理を切り替え、終局後の点数を求める。
このfinalScores自体をuseStateで持つのではなく、元になる入力値から計算する形にしている。
順位や必要点数も計算する
終局後点数が分かれば、そこから順位も計算できる。
const ranking = useMemo(
() => calculateRanking(finalScores),
[finalScores],
);
さらに、
- 現在の順位
- 自分の現在順位
- 目標達成済みか
- 必要なロン点
- 必要なツモ点
- 最低和了点
- 終局後に目標達成できたか
などもuseGameDerivedStateで計算する。
こうして見ると、useGameStateとの違いが分かりやすい。
ユーザーが変更する値
↓
useGameState
入力値から求められる値
↓
useGameDerivedState
という分け方になった。
App.tsxは画面を組み立てる役割にする
2つのカスタムフックを作ったことで、App.tsxではまず必要な値を受け取る。
const {
scores,
selfSeat,
goal,
honba,
kyotaku,
updateScore,
randomizeCondition,
} = useGameState();
次に、その値を計算用のフックへ渡す。
const {
finalScores,
ranking,
requiredRonList,
requiredTsumo,
success,
} = useGameDerivedState({
scores,
selfSeat,
goal,
honba,
kyotaku,
// ほかの入力値
});
あとは、それぞれの画面へ必要な値を渡す。
<ScoreInputSection
scores={scores}
onChangeScore={updateScore}
/>
<RequiredPointsSection
requiredRonList={requiredRonList}
requiredTsumo={requiredTsumo}
/>
<ResultSummarySection
finalScores={finalScores}
ranking={ranking}
success={success}
/>
App.tsxにもまだ渡している値は多いけれど、
「状態を管理する」
「点数を計算する」
「画面を表示する」
という役割は以前より分けやすくなった。
状態と計算結果を分けてみて
最初は、必要な値を全部useStateで持てばいいと思っていた。
ただ、今回整理してみると、同じ「値」でも少し種類が違う。
たとえば現在の点数はユーザーが入力するので状態として持つ必要がある。
一方、現在順位は点数があれば計算できる。
点数
↓
順位
なので、点数と順位を両方別々の状態として持つ必要はない。
必要な元データだけを状態として持ち、そこから求められるものは計算する。
この考え方は、今後Reactで機能を増やすときにも使えそう。
今回できたこと
今回の進捗は以下のとおり。
- Reactの状態管理を
useGameStateへまとめた - 保存データの読み込みと自動保存をまとめた
- ランダム条件生成を状態管理側へ移した
- 入力内容のリセット処理をまとめた
- 計算結果を
useGameDerivedStateへ分けた - 終局後点数と順位の計算を整理した
- 必要なロン点・ツモ点の計算を整理した
App.tsxを画面を組み立てる役割へ寄せた
新しい機能を追加した回ではないので、画面上の変化はほぼない。
それでも、今後さらに処理を増やしていくことを考えると、早めに整理しておいてよかったと思う。
コードが増えてくると、とりあえず動かすだけでは少しずつ厳しくなってくる。
次は、入力ミスや成立しない終局結果を判定するために作った、バリデーション処理についてまとめる。
今回も画面は特に変わりなし。牛一。
コメント