
この記事では、ポートフォリオ「Quizopia」のコードの一部を説明しています。
Contents
PokemonCryQuizController.php の一部紹介
// 問題セットを生成するプライベートメソッド
private function generateQuestionSet()
{
$questions = [];
$usedPokemonIds = [];
// 5問の問題を生成
for ($i = 0; $i < 5; $i++) {
do {
$randomPokemonId = rand(1, 251);
} while (in_array($randomPokemonId, $usedPokemonIds));
$usedPokemonIds[] = $randomPokemonId;
$options = $this->generateOptions($randomPokemonId);
$questions[] = [
'pokemonId' => $randomPokemonId,
'options' => $options,
];
}
return $questions;
}
// 選択肢を生成するプライベートメソッド
private function generateOptions($correctId)
{
$options = [$correctId];
// 正解以外の3つの選択肢を生成
while (count($options) < 4) {
$randomId = rand(1, 251);
if (!in_array($randomId, $options)) {
$options[] = $randomId;
}
}
// 選択肢をシャッフル
shuffle($options);
return $options;
}
1. generateQuestionSet()
関数
- 5つのランダムなポケモン問題を生成
- 各問題について重複しないポケモンIDを選択(1-251の範囲)
- 各問題に4つの選択肢を設定
2. generateOptions($correctId)
関数
- 正解のポケモンID (
$correctId
) と、他3つのランダムなポケモンIDを選択肢として生成 - 選択肢をシャッフルして返す
コード実行途中のイメージ例
// generateQuestionSet内の処理
$randomPokemonId = 25; // 例: ピカチュウのIDが選ばれたとします
$usedPokemonIds = [25]; // 使用済みIDリストに追加
// generateOptions(25)の呼び出し
$options = [25]; // 最初に正解のIDを追加
// 3つのランダムなIDを追加(重複しないように)
$options = [25, 4, 129, 150]; // 例: フシギソウ、コイキング、ミュウツーが追加されたとする
// シャッフル後
$options = [129, 25, 150, 4]; // シャッフル後はこのような順序になる可能性がある
// generateQuestionSetに戻り、問題を追加
$questions = [
[
'pokemonId' => 25, // ピカチュウのID
'options' => [129, 25, 150, 4] // シャッフルされた選択肢
]
];
5回分のループが完了した後(最終状態)
$usedPokemonIds = [25, 149, 6, 151, 39]; // 選ばれた5匹のポケモンID
$questions = [
[
'pokemonId' => 25, // ピカチュウ
'options' => [129, 25, 150, 4] // コイキング, ピカチュウ, ミュウツー, フシギソウ
],
[
'pokemonId' => 149, // カイリュー
'options' => [149, 36, 78, 94] // カイリュー, ピクシー, ギャロップ, ゲンガー
],
[
'pokemonId' => 6, // リザードン
'options' => [123, 6, 45, 249] // ストライク, リザードン, ラフレシア, ルギア
],
[
'pokemonId' => 151, // ミュウ
'options' => [74, 151, 114, 130] // イシツブテ, ミュウ, モンジャラ, ギャラドス
],
[
'pokemonId' => 39, // プリン
'options' => [39, 145, 88, 201] // プリン, サンダース, ベトベトン, アンノーン
]
];
3. playRandom 関数 – クイズを開始する処理
public function playRandom()
{
// 新しい問題セットを生成
$questions = $this->generateQuestionSet();
Session::put('quiz_questions', $questions);
Session::put('current_question_index', 0);
Session::put('score', 0);
// 現在の問題を取得
$currentIndex = Session::get('current_question_index');
$questions = Session::get('quiz_questions');
$currentQuestion = $questions[$currentIndex];
return Inertia::render('Play/RandomPokemonCry', [
'pokemonId' => $currentQuestion['pokemonId'],
'options' => $currentQuestion['options'],
'questionCount' => $currentIndex + 1,
'score' => Session::get('score'),
]);
}
この関数はクイズの開始時に実行され、ランダムな問題を生成してクイズの最初の画面を表示します。
具体的な処理の流れ
generateQuestionSet()
を呼び出して5問のランダムな問題を作成- 問題セット、現在の問題インデックス(0)、スコア(0)をセッションに保存
- 最初の問題(インデックス0)の情報を取得
- Inertia.jsを使ってフロントエンド画面を表示するためのデータを渡す
以下は具体的なイメージです。
// 生成された$questions配列の例(セッションに保存される)
$questions = [
[
'pokemonId' => 25, // ピカチュウ
'options' => [129, 25, 150, 4] // コイキング, ピカチュウ, ミュウツー, フシギソウ
],
// ... 以下4問分の問題データ
];
// フロントエンドに渡されるデータのイメージ
[
'pokemonId' => 25, // この音声が再生される
'options' => [129, 25, 150, 4], // 選択肢として表示される
'questionCount' => 1, // 「1/5問目」と表示
'score' => 0 // 現在のスコア
]
4. nextQuestion 関数 – 次の問題へ進む処理
public function nextQuestion()
{
$currentIndex = Session::get('current_question_index',0);
$questions = Session::get('quiz_questions', []);
$currentIndex++;
if ($currentIndex >= 5) {
// クイズ終了
Session::forget(['quiz_questions', 'current_question_index']);
return redirect()->route('play.random-pokemon-cry');
}
Session::put('current_question_index', $currentIndex);
return Inertia::render('Play/RandomPokemonCry', [
'pokemonId' => $currentQuestion['pokemonId'],
'options' => $currentQuestion['options'],
'questionCount' => $currentIndex + 1,
'score' => Session::get('score', 0),
]);
}
ユーザーが「次へ」ボタンを押した時に実行され、次の問題に進めるか、クイズを終了するかを制御します。
具体的な処理の流れ:
- 現在の問題インデックスを取得して1増やす
- もし問題が5問すべて終わったら(インデックスが5以上):
- セッションから問題関連のデータを削除
- クイズ開始画面にリダイレクト(新しいクイズがスタート)
- まだ問題が残っている場合:
- 新しいインデックスをセッションに保存
- 同じ画面にリダイレクト(次の問題が表示される)
以下は具体的なイメージです。
/ セッションの状態変化
// 例:2問目が終わって3問目に進む場合
// 変更前
Session::get('current_question_index') = 1; // 2問目(0からカウント)
// 変更後
Session::put('current_question_index', 2); // 3問目に更新
// 例:最後の問題(5問目)が終わった場合
// 変更前
Session::get('current_question_index') = 4; // 5問目
// 変更後
// 新しいクイズを開始
5. updateScore 関数 – スコアを更新する処理
public function updateScore()
{
$currentScore = Session::get('score', 0);
Session::put('score', $currentScore + 1);
return response()->json(['success' => true]);
}
ユーザーが正解を選んだときに呼び出され、スコアを1点増やします。
具体的な処理の流れ:
- 現在のスコアをセッションから取得
- スコアに1を加えて更新
- 成功したことを示すJSONレスポンスを返す(Ajaxリクエスト用)
以下は具体的なイメージです。
// 例:現在のスコアが2点で正解した場合
// 変更前
Session::get('score') = 2;
// 変更後
Session::put('score', 3); // 3点に更新
// フロントエンドに返すレスポンス
{ "success": true }
6. resetQuiz 関数 – クイズをリセットする処理
public function resetQuiz()
{
// セッションを完全にクリア
Session::forget(['quiz_questions', 'current_question_index', 'score']);
return redirect()->route('play.random-pokemon-cry');
}
クイズを中断して最初からやり直したい場合などに、クイズデータをリセットします。
具体的な処理の流れ:
- クイズ関連のセッションデータをすべて削除
- 成功したことを示すJSONレスポンスを返す
以下は具体的なイメージです。
// セッションから以下のデータが削除される
- 'quiz_questions'(問題セット)
- 'current_question_index'(現在の問題インデックス)
- 'score'(現在のスコア)
// フロントエンドに返すレスポンス
{ "success": true }
フロントエンド部分の説明(RandomPokemonCry.tsx)
1. コンポーネント構造と状態管理
RandomPokemonCry = ({
pokemonId,
options,
questionCount,
score: initialScore,
}: {
pokemonId: number;
options: number[];
questionCount: number;
score: number;
}) => {
// State管理
const [pokemons] = useState<Pokemon[]>(pokemonData);
const [currentQuiz, setCurrentQuiz] = useState<CurrentQuiz | null>(null);
const [selectedAnswer, setSelectedAnswer] = useState<Pokemon | null>(null);
const [showResult, setShowResult] = useState<boolean>(false);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [isQuizFinished, setIsQuizFinished] = useState<boolean>(false);
const [score, setScore] = useState<number>(initialScore);
const TOTAL_QUESTIONS = 5;
このコンポーネントはサーバーから受け取った情報(pokemonId, options, questionCount, score)によって、ポケモンのなき声クイズを表示します。具体的には、以下の状態を管理しています。
- pokemons: 全ポケモンデータの配列(JSONファイルからインポート)
- currentQuiz: 現在の問題データ(選択肢と正解)
- selectedAnswer: ユーザーが選んだ回答
- showResult: 回答結果を表示するかどうか
- isLoading: 音声ロード中かどうか
- isQuizFinished: クイズが終了したかどうか
- score: 現在のスコア
2. 音声再生機能
// オーディオ参照
const audioRef = useRef<HTMLAudioElement | null>(null);
// 音声再生関数
const playSound = async () => {
if (isLoading) return;
try {
setIsLoading(true);
// 音声ファイルのパスを設定(M4A形式を使用)
const paddedId = pokemonId.toString().padStart(3, "0");
const audioPath = `/audio/pokemon_cries/${paddedId}.m4a`;
// 既存のオーディオ要素をクリーンアップ
if (audioRef.current) {
audioRef.current.pause();
audioRef.current.removeAttribute("src");
audioRef.current.load();
}
// HTML5 Audio要素を使用
if (!audioRef.current) {
audioRef.current = new Audio();
}
// ...イベントリスナー設定、再生処理など
} catch (error) {
// エラーハンドリング
}
};
この部分はポケモンの鳴き声を再生するための機能です。
- audioRef: HTML Audio要素への参照
- playSound関数: 選択されたポケモンIDに基づいて音声ファイルをロードして再生
- エラーハンドリング: 特にiOSデバイスでの再生問題に対応する詳細な処理
3. クイズゲームのロジック
// 回答チェック関数
const checkAnswer = async (pokemon: Pokemon) => {
try {
setSelectedAnswer(pokemon);
setShowResult(true);
if (pokemon.pokeapi_id === currentQuiz?.correct.pokeapi_id) {
// 正解の場合、スコアを更新
const response = await axios.post(route("play.update-score"));
setScore(response.data.score || score + 1);
}
} catch (error) {
console.error("回答処理エラー:", error);
alert("エラーが発生しました。もう一度お試しください。");
}
};
// クイズ生成関数
const generateQuiz = () => {
if (isQuizFinished) return;
// 問題数が上限に達した場合は結果画面を表示
if (questionCount > TOTAL_QUESTIONS) {
setIsQuizFinished(true);
return;
}
const selectedPokemons = options
.map((id) => pokemons.find((p) => p.pokeapi_id === id))
.filter((p): p is Pokemon => p !== undefined);
const correctAnswer = selectedPokemons.find(
(p) => p.pokeapi_id === pokemonId
);
if (correctAnswer) {
setCurrentQuiz({
choices: selectedPokemons,
correct: correctAnswer,
});
setSelectedAnswer(null);
setShowResult(false);
}
};
上記の関数がクイズゲームの中核ロジックを実装しています。
- checkAnswer: ユーザーが選んだ回答が正解かどうかをチェックし、正解ならplay.update-scoreエンドポイントを呼び出してスコアを更新
- generateQuiz: サーバーから受け取った選択肢IDを実際のポケモンオブジェクトに変換し、クイズ問題を構築
4. ゲーム進行管理
// 次の問題へ進む関数
const handleNextQuestion = () => {
if (questionCount >= TOTAL_QUESTIONS) {
setIsQuizFinished(true);
} else {
router.get(route("play.next-question"));
}
};
// クイズをリセットする関数
const resetQuiz = () => {
setIsLoading(true); // ボタンを無効化してユーザー操作を防ぐ
router.get(route("play.reset-pokemon-cry"));
};
上記の関数は、ゲームの進行を管理します。
- handleNextQuestion: 現在の問題が最後なら結果画面に移動、そうでなければサーバーに次の問題をリクエスト
- resetQuiz: クイズをリセットしてサーバーから新しい問題セットを取得
5. 副作用とイニシャライズ
// 初回マウント時にクイズを生成
useEffect(() => {
// 問題数が上限に達した場合は結果画面を表示
if (questionCount > TOTAL_QUESTIONS) {
setIsQuizFinished(true);
} else {
generateQuiz();
}
// モバイルデバイスの場合、オーディオ初期化のためのダミー要素を作成
if (isMobile) {
// オーディオ要素の初期化と各種イベントリスナーの設定
}
return () => {
// クリーンアップ処理
};
}, [pokemonId, options, questionCount]);
このuseEffect部分は、
- コンポーネントのマウント時にクイズを初期化
- 問題数が上限を超えている場合は結果画面を表示
- モバイルデバイス(特にiOS)向けの音声再生初期化処理
- コンポーネントのアンマウント時のクリーンアップ
6. UI部分
return (
<PlayMainLayout>
<div className="container mx-auto px-4 py-8">
{/* 非表示のオーディオ要素 */}
<audio
ref={audioRef}
style={{ display: "none" }}
preload="none"
playsInline
/>
<Card className="w-full max-w-2xl mx-auto">
<CardHeader>
<CardTitle className="text-center text-2xl">
ポケモン なきごえ クイズ
</CardTitle>
<div className="text-center text-lg">
{!isQuizFinished && (
<div>
もんだい {questionCount}/{TOTAL_QUESTIONS}
</div>
)}
<div>スコア: {score}</div>
</div>
</CardHeader>
<CardContent>
{!isQuizFinished ? (
<>
{/* なきごえを聞くボタン */}
<div className="flex justify-center mb-8">
<Button onClick={handlePlayButtonClick} /* ... */}>
<Volume2 className="w-6 h-6" />
{isLoading
? "よみこみちゅう..."
: "なきごえを きく"}
</Button>
</div>
{/* 選択肢ボタン */}
<div className="grid grid-cols-2 gap-4">
{currentQuiz?.choices.map((pokemon) => (
<Button
key={pokemon.pokeapi_id}
onClick={() => checkAnswer(pokemon)}
disabled={showResult}
variant={/* 条件付きスタイル */}
>
{pokemon.pokeapi_species_name_ja}
</Button>
))}
</div>
{/* 結果表示と次の問題ボタン */}
{showResult && (
<div className="mt-8 text-center">
<p className="text-xl mb-4">
{selectedAnswer?.pokeapi_id ===
currentQuiz?.correct.pokeapi_id
? "せいかい! 🎉"
: `ざんねん... せいかいは「${currentQuiz?.correct.pokeapi_species_name_ja}」でした`}
</p>
<Button
onClick={handleNextQuestion}
className="text-lg"
>
{questionCount === TOTAL_QUESTIONS
? "けっかを みる"
: "つぎの もんだい"}
</Button>
</div>
)}
</>
) : (
// クイズ終了時の結果表示
<div className="text-center">
<h2 className="text-2xl mb-4">
クイズ しゅうりょう!
</h2>
<p className="text-xl mb-4">
{TOTAL_QUESTIONS}もんちゅう {score}もん せいかい!
</p>
<Button onClick={resetQuiz} className="text-lg">
もういちど ちょうせん する
</Button>
</div>
)}
</CardContent>
</Card>
</div>
</PlayMainLayout>
);
UIはクイズの状態に応じて異なる内容を表示しています。
クイズ進行中
- 問題番号とスコア表示
- 「なきごえを きく」ボタン
- 4つの選択肢ボタン
- 回答後の結果表示と次の問題へ進むボタン
クイズ終了時
- 結果概要(全5問中何問正解したか)
- 「もういちど ちょうせん する」ボタン
まとめ. フロントエンド・バックエンド連携フロー
1.初期表示時
- サーバー側のplayRandomメソッドが5問分の問題データを生成
- 最初の問題データをフロントエンドに送信
- フロントエンドのuseEffectでgenerateQuizを実行し、UIを構築
2.回答選択時
- checkAnswer関数が実行され、選択されたポケモンが正解かチェック
- 正解の場合、axios.post(route(“play.update-score”))でサーバーにスコアを更新させる
- 結果表示と「つぎのもんだいへ」ボタンを表示
3.次の問題へ進む時
- handleNextQuestion関数が実行され、router.get(route(“play.next-question”))で次の問題をリクエスト
- サーバー側のnextQuestionメソッドが問題インデックスを進め、次の問題データを返す
- 新しい問題データでページが再レンダリングされる
4.クイズ終了時
- 最終問題の結果表示後、結果画面に切り替わる
- 「もういちどちょうせんする」ボタンが表示される
- ボタンクリックでresetQuiz関数が実行され、router.get(route(“play.reset-pokemon-cry”))でクイズをリセット
- サーバー側で新しい問題セットが生成され、最初からクイズが始まる
このフロントエンドの実装はサーバーサイドのコントローラーと連携して、双方向の通信でクイズゲームの状態を管理しています。