はじめに
皆さんはhttpステータスコードをいくつ覚えているでしょうか?
私は500番台がサーバーエラーで、400番台がクライアント側のエラー、200が成功で……ぐらいのざっくり有名なステータスコードぐらいしか覚えられてませんでした。
今回は、httpステータスコードを覚えるためにゲームを作ってみたので紹介したいと思います。
このゲームを作ろうと思ったきっかけ
きっかけは「HTTPステータスコード百人一首」というボードゲームをやってからでした。
どういうゲームかというと、httpステータスコードの説明文章を1人が読み上げて、他のプレイヤーがそれだと思う番号のかかれたカードを取ります。
正解であればそのカードを獲得でき、一番カードを多く手に入れた人が勝ちというゲームです。
このゲームをやったあと、楽しみながらステータスコードを覚えられるのは良いなと思い、いつでも気軽に1人でもステータスコードを覚えられるゲームを作ってみようと思いました。
開発したゲームの紹介
まずはタイトル画面でユーザー名を入力します。
入力したら、ゲームスタートをクリック!
ゲーム画面では上にランダムで問題文が表示されます。制限時間60秒の間にできるだけ多く正解を目指します。
選択肢から、問題の答えだと思うステータス番号をクリックします。
正解であれば、正解と表示され、次の問題に進めます。
不正解であれば、不正解と表示され、再度やり直しとなります。
問題に正解するまでは次の問題に進めません。
60秒立てば、ゲーム終了です。
点数が表示され、自分のランキングを見ることができます。
ランキングできるだけ多く正解し、ランキング1位を目指しましょう!
作ろうと決まれば、まずは計画
ゲームを作ろうと思ったのですが、ざっくりこういう感じかなというふんわりとした構想しか頭の中にありませんでした。
そのため、まずはTODOリストを作ることにしました。
MVP開発で進めていくために、最小限の機能ごとにやることを書き出していきました。
書き出してみると、DBのカラムで考慮が漏れていて足りない部分が分かったり、実装の際にどうつくっていばいいか具体的にイメージがつきやすくなりました。
言語に関しては、最近Reactを使っているのでReactで作ることにしました。
(簡環境構築が用意で開発がやりやすいと思ったのも理由)
一部ですが、このような感じでhackMDにまとめています。
大体の大枠が決まれば、実際に開発にとりかかりました。
開発期間はTODO作成から開発後の調整を含めて、計3日間ほどです。
使用した技術
言語はReact, TypeScriptで作成しています。また、supabaseに問題文やランキングデータを保存しています。
firebaseにデプロイし、どこからでもアクセスできるようにしています。
開発効率のために、GithubActionsでpushされたらデプロイされるように自動デプロイしょrを組んでいます。
名前 | 説明 |
---|---|
使用言語 | React, TypeScript |
UI | Tailwind CSS |
データベース | Supabase |
サーバーサイド | Firebase |
自動デプロイ | GithubActions |
実装していて難しかった点
難しかった点としては、やはりゲームの点数計算、ゲームバランスを考えるということでした。
点数計算について
点数計算はパターンとしては以下の2通りあります。
- 正解であれば、加算する
- 不正解であれば、加算しない
この処理自体は単純なのですが、表示している問題とクリックしたhttpステータスコードの選択肢のどれが正解のステータスコードかという処理に頭を悩まされました。
なぜかというと選択肢は18件までしか表示していません。問題文のパターンは61件あるため、ただ選択肢を上から18件取得して表示しても、問題文の正解となるステータスコードが表示されてないということになってしまうためです。
絶対に正解できないゲームとなってしまいます。
また、60秒時間がたったらそこでゲームを終了し、ユーザーの点数データを更新しないといけません。
これらの考慮に悩みましたが、最終的には以下のように処理を作成しました。
問題取得部分
最初にuseEffectの以下の部分でランダムに18件の問題文を取得します。
allStatusはすべての問題データ、selectedStatusは問題と選択肢として表示される18件のデータです。
randomStatusは18件のデータから1件だけ問題文として抽出しています。
useEffect(() => {
// すべての問題データを取得
let newAllStatus = await getAllStatus();
setAllStatus(newAllStatus);
// すべての問題データから18件ランダムに取得
newAllStatus = shuffleArray(newAllStatus).slice(0, 18);
setSelectedStatus(newAllStatus);
// ランダムでステータスデータ取得
const randomIndex = Math.floor(Math.random() * newAllStatus.length);
setRandomStatus(newAllStatus[randomIndex]);
// 省略
}, [id]);
// 問題をランダムに取得する関数
const shuffleArray = (array: Status[]) => {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
return array;
};
カード初期値設定部分
選択1つ1つのカードの初期値を2としています。
これが通常のカードの状態です。正解であれば1に変更し、不正解であれば2に変更します。
画面のカードの表示を正解、不正解の際で切り替えるために使っている処理です。
// cardStatusの初期値を設定
const initialCardStatus = newAllStatus.reduce((acc, status) => {
acc[status.id] = 2; // 2は通常状態を表す
return acc;
}, {} as { [key: number]: number });
setCardStatus(initialCardStatus);
カウント処理部分
60秒のカウント処理を行っています。もし60秒たったならば、ユーザーのランキングデータを更新します。
// カウント処理
useEffect(() => {
const interval = setInterval(() => {
setElapsedTime(prevTime => {
if (prevTime <= 1) {
clearInterval(interval);
updateAndNavigate();
}
return prevTime - 1;
});
}, 1000);
// 最終的なランキングデータアップデート処理
const updateAndNavigate = async () => {
console.log(point);
await updateRanking(Number(id), point);
navigate(`/result/${id}`);
};
正誤判定部分
judgemtQuestionで正誤判定を行っています。問題文のidとクリックしたカードのidが一致すれば、正解のカード状態に変更を行います。不正解であれば不正解のカード状態に変更を行います。
// 正誤判定処理
const judgemtQuestion = (id: number) => {
if (randomStatus?.id !== id) {
setCardStatus(prevStatus => ({ ...prevStatus, [id]: 0 }));
return;
}
setCardStatus(prevStatus => ({ ...prevStatus, [id]: 1 }));
}
正解時の処理
handleNextQuestionで正解時の処理を行っています。
正解時は、問題を再取得し、selectedStatusやrandomStatusに格納し直します。
また、点数もpointに加算します。
// 次の問題をクリックした際の処理
const handleNextQuestion = (id: number) => {
setCardStatus(prevStatus => ({ ...prevStatus, [id]: 2 }));
// ステータスデータを削除してランダムでステータスデータを取得
const updatedAllStatus = allStatus.filter(status => status.id !== id);
setAllStatus(updatedAllStatus);
// cardStatusの初期値を設定
const initialCardStatus = updatedAllStatus.reduce((acc, status) => {
acc[status.id] = 2; // 2は通常状態を表す
return acc;
}, {} as { [key: number]: number });
setCardStatus(initialCardStatus);
// 新しいステータスデータを取得
const newSelectedStatus = shuffleArray(updatedAllStatus).slice(0, 18);
setSelectedStatus(newSelectedStatus);
const randomIndex = Math.floor(Math.random() * newSelectedStatus.length);
setRandomStatus(newSelectedStatus[randomIndex]);
// ポイントを加算
setPoint(point + 1);
}
不正解時の処理
handleRetryでリトライ時の処理を行います。
リトライをクリックするとカードが通常状態の2に戻ります。
// リトライ時の処理
const handleRetry = (id: number) => {
setCardStatus(prevStatus => ({ ...prevStatus, [id]: 2 }));
}
ゲームバランスについて
ゲームはどこかにはまる要素がないと継続してできないと思います。
最初の想定では問題文が表示され、クリックしたステータスコードが正解か不正解かを表示するだけのゲームにしようと考えていました。
ただ、それだけだと面白みに欠けるし、継続してやることはできないなと感じました。
ステータスコードを覚えるためにゲームを作ったのに、やらなければ意味がないなと思います。
そこで3つの要素を用意しました。
- 制限時間を設ける
- 正解した点数をつける
- ランキングを導入する
制限時間と点数に関しては、限られた時間内でどれだけ正解できるかという要素を設け、今回は点数が低かったからもっと高い点数目指してみようというモチベーションを高められると思ったからです。
また、ランキングはその派生で思いついたものですが、異なるユーザーがいた場合ほかの人との競争心を煽ることができると考えました。
自分だけしかプレイしてなくても、前の記録を見返すことができるため、次の目標を設定しやすいと考えました。
実際にやってみて
実際にプレイしてみて、制限時間内でできるだけ多く正解するというものはゲームとしてよかったと思いました。
同じ問題が何度か表示されれば、以前間違えた問題で正解はこれだと考えてゲームをしながら学ぶことができるのは良いと思いました。
ただ、問題文が所見では難しかったのと、制限時間60秒は短すぎたなという印象でした。
MDN Web Docsに掲載されているステータスコードの説明文をそのまま問題文にしたのですが、あまり見かけないステータスコードや、よく見かけるステータスコードでも説明がわかりにくところありました。
また、初見では考える時間を多く用意しないとすぐ時間切れになってしまいます。
改善
まずは問題文をGPTになげてもっとわかりやすい文章に変更してもらいました。
例として、307ステータスコードの文章をわかりやすくしてもらった載せます。
修正前
307,Temporary Redirect,サーバーはこのレスポンスを、リクエストされたリソースを別の URI で、元のリクエストと同じメソッドを使用して取得するようクライアントを誘導するために送信します。 これは 302 Found HTTP レスポンスコードと同じ意味を持ちますが、ユーザーエージェントは使用する HTTP メソッドを変更してはならない点が異なります。始めのリクエストで POST を用いた場合は、次のリクエストでも POST を使用しなければなりません。
修正後
307,Temporary Redirect,一時的に別の URI でリソースを取得するようにクライアントに指示するレスポンス。
変更してみて、問題文を理解しやすくなり、問題を解きやすくなりました。
また、制限時間を3分に変更しました。長すぎず、短すぎない時間に調整しました。
課題感
実装はまだできていませんが、ヒントボタンを押したら問題のヒントがみれるようになればさらに初見でもゲームがやりやすくなると思いました。
また、不正解で時間切れとなってしまったら、問題の答えを表示してあげればさらに学習の質を向上できると感じました。
余裕がある時に、すこしずつ改善していきます。
おわりに
ゲームを作ったら、それだけで満足してしまいがちですが、目的はhttpステータスコードを覚えることです。
学習の息抜きや、ちょっとした隙間時間に遊びながらステータスコードを覚えていきたいと思います。
この記事がどなたかの役に立てば幸いです。