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

前回までの進捗
前回は、Reactのカスタムフックを使って状態管理と計算処理を整理した。
入力値はuseGameState、入力値から求められる結果はuseGameDerivedStateへ分けたことで、処理の役割が以前より分かりやすくなった。
今回は、入力された値がおかしくないか確認するバリデーション処理を整理した。
このアプリでは点数などを自由に入力できるため、
- 25,050点のような100点単位ではない持ち点
- マイナスの本場
- ロンなのに和了者と放銃者が同じ
- 0点のロン
- おかしなツモ支払い
なども入力できてしまう。
そのまま計算すると意味のない結果を表示してしまうため、計算前に入力値を確認することにした。
バリデーションとは
バリデーションは、入力された値が決めたルールに合っているか確認する処理。
今回なら、
持ち点は100点単位か
本場は0以上の整数か
供託は0以上の整数か
ロンの和了者と放銃者は別か
ロン点は正しい形式か
ツモ支払いは正しい形式か
といったことを確認する。
処理はvalidation.tsへまとめた。
画面側や点数計算の中に個別で書くのではなく、入力チェックを一か所に集める形にしている。
持ち点が100点単位か確認する
麻雀の持ち点は100点単位で扱っているため、100で割り切れるか確認する。
export function isValidPointUnit(
points: number,
): boolean {
return (
Number.isFinite(points) &&
points % 100 === 0
);
}
4人分の持ち点については、someを使って一人でも条件に合わない人がいるか確認している。
const hasInvalidScore = seats.some(
(seat) =>
!isValidPointUnit(scores[seat]),
);
たとえば、
東家 25000
南家 25100
西家 24900
北家 25050
なら、北家だけ100点単位になっていないのでエラーになる。
本場と供託は0以上の整数にする
本場と供託は、
0
1
2
3
...
のような整数で扱う。
そのため、
export function isValidNonNegativeInteger(
value: number,
): boolean {
return (
Number.isFinite(value) &&
Number.isInteger(value) &&
value >= 0
);
}
という関数でチェックする。
const hasInvalidHonba =
!isValidNonNegativeInteger(honba);
const hasInvalidKyotaku =
!isValidNonNegativeInteger(kyotaku);
これで、
-1本場
1.5本場
-2本の供託
などはエラーになる。
ロン時の入力を確認する
ロンでは、特有の入力ミスもある。
まず、和了者と放銃者が同じ人物になることはない。
winner === loser
になっていたらエラーにする。
さらに、ロン点についても確認する。
const isInvalidRon =
resultType === "ron" &&
(
winner === loser ||
ronPoints <= 0 ||
!isValidPointUnit(ronPoints)
);
つまりロンの場合は、
- 和了者と放銃者が違う
- ロン点が1点以上
- 100点単位
である必要がある。
たとえば、
和了者:南家
放銃者:南家
ロン点:3900
なら、点数自体は問題なくても和了者と放銃者が同じなのでエラーになる。
ツモ時の入力を確認する
ツモも、支払い点数を確認する。
const isInvalidTsumo =
resultType === "tsumo" &&
(
dealerPayment <= 0 ||
!isValidPointUnit(dealerPayment) ||
(
winner !== dealerSeat &&
(
childPayment <= 0 ||
!isValidPointUnit(childPayment)
)
)
);
親の支払いは、
- 1点以上
- 100点単位
である必要がある。
子がツモした場合は、子の支払いについても同じ条件で確認する。
一方、親ツモなら全員同じ金額を支払うため、子用の支払い値は判定に使わない。
終局方法によって必要な入力が違うので、バリデーション側でもロンとツモを分けている。
エラーが一つでもあれば計算しない
それぞれの判定結果をまとめて、最終的にエラーがあるか確認する。
const hasError =
hasInvalidScore ||
hasInvalidHonba ||
hasInvalidKyotaku ||
isInvalidRon ||
isInvalidTsumo;
どれか一つでもtrueなら、hasErrorもtrueになる。
この値を点数計算側で確認し、入力がおかしい場合は終局後の点数計算を行わないようにした。
たとえばロンの和了者と放銃者を同じにしても、その状態で無理に点数移動を計算しない。
入力値を自由に変更できる画面なので、計算処理側へ変な値を渡さないための安全装置として使っている。
エラー内容も表示する
単に「エラーです」と表示するだけでは、どこを直せばいいか分からない。
そこで、入力内容に応じてエラーメッセージも変えることにした。
if (hasInvalidScore) {
return "エラー:持ち点は100点単位で入力してください";
}
if (hasInvalidHonba) {
return "エラー:本場は0以上の整数で入力してください";
}
if (hasInvalidKyotaku) {
return "エラー:供託は0以上の整数で入力してください";
}
ロンの場合なら、
if (
resultType === "ron" &&
winner === loser
) {
return "エラー:和了者と放銃者が同じです";
}
とする。
ほかにも、
エラー:ロン点は1点以上を入力してください
エラー:ロン点は100点単位で入力してください
エラー:親の支払いは100点単位で入力してください
エラー:子の支払いは100点単位で入力してください
など、原因に応じたメッセージを返す。
これで、入力がおかしいことだけでなく、何を修正すればいいかも画面上で分かるようになった。
判定結果をまとめて返す
validateGameInputでは、最終的なhasErrorだけでなく、それぞれの判定結果も返している。
return {
hasInvalidScore,
hasInvalidHonba,
hasInvalidKyotaku,
isInvalidRon,
isInvalidTsumo,
hasError,
errorMessage,
};
こうしておくと、
点数がおかしいのか
本場がおかしいのか
終局結果がおかしいのか
を呼び出し側でも判別できる。
必要点数の計算では持ち点・本場・供託のエラーだけ確認し、仮の終局結果の入力ミスとは分けて扱うこともできる。
最初から単純なtrueかfalseだけを返すより、あとで使いやすい形になった。
バリデーションを分けてみて
入力チェック自体は、一つ一つを見るとそれほど複雑ではない。
ただ、画面や点数計算の中へ直接書き始めると、
ここでも100点単位を確認
あそこでも本場を確認
ロンのところでも確認
ツモのところでも確認
という形になりやすい。
今回はvalidation.tsへ集めたことで、
入力値
↓
validateGameInput
↓
問題なし → 計算
問題あり → エラー表示
という流れにできた。
機能が増えて入力項目も増えてきたので、こういう処理を早めに分けておくと後から修正しやすそう。
今回できたこと
今回の進捗は以下のとおり。
- 持ち点が100点単位か確認するようにした
- 本場が0以上の整数か確認するようにした
- 供託が0以上の整数か確認するようにした
- ロン時の和了者と放銃者を確認した
- ロン点の入力値を確認した
- ツモ時の支払い点数を確認した
- 入力エラー時は点数計算を行わないようにした
- エラー原因に応じたメッセージを表示できるようにした
- バリデーション処理を別ファイルへまとめた
見た目としてはエラーメッセージが増えた程度だけれど、自由入力できるアプリとしては必要な処理が一つ増えた。
点数計算そのものだけでなく、「おかしな値が入ってきたときにどうするか」まで考える必要があるのは、実際に画面を作ってみて分かった部分だった。
ロジカルな部分で書いてて面白いが、画面に変化がなく進捗が見えづらい。牛一。
コメント