Added quiz features
This commit is contained in:
200
app/Http/Controllers/api/PvPQuizController.php
Normal file
200
app/Http/Controllers/api/PvPQuizController.php
Normal file
@@ -0,0 +1,200 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Question;
|
||||
use App\Models\PvpMatch;
|
||||
use App\Traits\BaseApiResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class PvPQuizController extends Controller
|
||||
{
|
||||
use BaseApiResponse;
|
||||
|
||||
/**
|
||||
* STEP 1: Join the Matchmaking Queue
|
||||
*/
|
||||
public function findMatch(Request $request)
|
||||
{
|
||||
$userId = auth()->id();
|
||||
|
||||
// 1. Check if anyone else is waiting in the cache queue
|
||||
$waitingPlayer = Cache::get('pvp_waiting_player');
|
||||
|
||||
// Ensure we don't match the user with themselves
|
||||
if ($waitingPlayer && $waitingPlayer != $userId) {
|
||||
|
||||
// MATCH FOUND! Remove the waiting player from the queue
|
||||
Cache::forget('pvp_waiting_player');
|
||||
|
||||
// Create the Game Room
|
||||
$match = PvpMatch::create([
|
||||
'status' => 'in_progress',
|
||||
'started_at' => now(),
|
||||
]);
|
||||
|
||||
// Add both players to the match
|
||||
$match->players()->createMany([
|
||||
['user_id' => $waitingPlayer, 'score' => 0],
|
||||
['user_id' => $userId, 'score' => 0],
|
||||
]);
|
||||
|
||||
// Select 5 random multiple-choice questions
|
||||
$questions = Question::where('type', 'multiple_choice')
|
||||
->inRandomOrder()
|
||||
->limit(5)
|
||||
->pluck('id');
|
||||
|
||||
// Attach questions to the match
|
||||
$match->questions()->attach($questions);
|
||||
|
||||
// TODO (Next Step): Fire a WebSocket Event here to tell Player 1 the match started!
|
||||
|
||||
return $this->success([
|
||||
'match_id' => $match->id,
|
||||
'status' => 'matched',
|
||||
'opponent_id' => $waitingPlayer,
|
||||
'questions' => $match->questions()->with('options')->get()->makeHidden(['solution_explanation'])->each(function($q) {
|
||||
$q->options->each->makeHidden(['is_correct']);
|
||||
})
|
||||
], 'موفق', 'حریف پیدا شد! بازی شروع میشود.');
|
||||
}
|
||||
|
||||
// NO MATCH FOUND YET. Put this user in the waiting queue for 30 seconds.
|
||||
Cache::put('pvp_waiting_player', $userId, 30);
|
||||
|
||||
return $this->success([
|
||||
'status' => 'waiting'
|
||||
], 'در حال جستجو', 'در حال یافتن حریف...');
|
||||
}
|
||||
|
||||
/**
|
||||
* STEP 2: Submit Answer for a single question (Fastest Finger Logic)
|
||||
*/
|
||||
public function submitAnswer(Request $request, PvpMatch $match)
|
||||
{
|
||||
$request->validate([
|
||||
'question_id' => 'required|exists:questions,id',
|
||||
'selected_option_id' => 'required|exists:question_options,id',
|
||||
'time_taken_seconds' => 'required|numeric|min:0.1|max:15', // Max 15 seconds to answer
|
||||
]);
|
||||
|
||||
$player = $match->players()->where('user_id', auth()->id())->firstOrFail();
|
||||
$question = Question::with('options')->find($request->question_id);
|
||||
|
||||
$isCorrect = false;
|
||||
$pointsEarned = 0;
|
||||
|
||||
// Check if correct
|
||||
$correctOption = $question->options->where('is_correct', true)->first();
|
||||
if ($correctOption && $correctOption->id == $request->selected_option_id) {
|
||||
$isCorrect = true;
|
||||
|
||||
// MATH: Speed Bonus Calculation
|
||||
// If base score is 10, and you answer in 2 seconds (out of 15), you get a bonus!
|
||||
$baseScore = 10;
|
||||
$maxTime = 15;
|
||||
$speedBonus = ($maxTime - $request->time_taken_seconds) * 0.5; // Half a point for every second saved
|
||||
|
||||
$pointsEarned = $baseScore + $speedBonus;
|
||||
}
|
||||
|
||||
// Save the answer
|
||||
$match->answers()->create([
|
||||
'user_id' => auth()->id(),
|
||||
'question_id' => $question->id,
|
||||
'selected_option_id' => $request->selected_option_id,
|
||||
'is_correct' => $isCorrect,
|
||||
'time_taken' => $request->time_taken_seconds,
|
||||
'points_earned' => $pointsEarned
|
||||
]);
|
||||
|
||||
// Update total score for the player
|
||||
$player->increment('score', $pointsEarned);
|
||||
|
||||
// ==========================================
|
||||
// CHECK IF THE MATCH IS OVER
|
||||
// ==========================================
|
||||
$totalQuestions = $match->questions()->count(); // Should be 5
|
||||
$totalExpectedAnswers = $totalQuestions * 2; // 5 questions * 2 players = 10 answers
|
||||
|
||||
// Count how many answers have been submitted in this match globally
|
||||
$currentTotalAnswers = $match->answers()->count();
|
||||
|
||||
$isMatchOver = false;
|
||||
|
||||
if ($currentTotalAnswers >= $totalExpectedAnswers) {
|
||||
// The final answer was just submitted! End the game.
|
||||
$this->finalizeMatch($match);
|
||||
$isMatchOver = true;
|
||||
}
|
||||
|
||||
return $this->success([
|
||||
'is_correct' => $isCorrect,
|
||||
'points_earned' => round($pointsEarned, 2),
|
||||
'current_total_score' => round($player->score, 2),
|
||||
'is_match_over' => $isMatchOver
|
||||
], 'موفق', 'پاسخ ثبت شد.');
|
||||
}
|
||||
|
||||
/**
|
||||
* STEP 3: Finalize the Match and Declare Winner (Internal Helper)
|
||||
*/
|
||||
private function finalizeMatch(PvpMatch $match)
|
||||
{
|
||||
// Get both players, ordered by the highest score first
|
||||
$players = $match->players()->orderByDesc('score')->get();
|
||||
|
||||
$player1 = $players->first(); // The one with the higher score
|
||||
$player2 = $players->last(); // The one with the lower score
|
||||
|
||||
$winnerId = null;
|
||||
|
||||
// Check if it's a draw, or if someone actually won
|
||||
if ($player1->score > $player2->score) {
|
||||
$winnerId = $player1->user_id;
|
||||
}
|
||||
// If scores are exactly equal, winnerId stays null (Draw/Tie)
|
||||
|
||||
// Close the room
|
||||
$match->update([
|
||||
'status' => 'completed',
|
||||
'ended_at' => now(),
|
||||
'winner_id' => $winnerId
|
||||
]);
|
||||
|
||||
// TODO (Next Step): Fire a WebSocket Event here to push the "Game Over" screen!
|
||||
}
|
||||
|
||||
/**
|
||||
* STEP 4: Get Final Match Results
|
||||
*/
|
||||
public function matchResult(PvpMatch $match)
|
||||
{
|
||||
// Ensure user was actually in this match
|
||||
if (!$match->players()->where('user_id', auth()->id())->exists()) {
|
||||
return response()->json(['message' => 'دسترسی غیرمجاز'], 403);
|
||||
}
|
||||
|
||||
$match->load([
|
||||
'players.user:id,name', // Load player names
|
||||
'winner:id,name' // Load winner name
|
||||
]);
|
||||
|
||||
return $this->success([
|
||||
'match_id' => $match->id,
|
||||
'status' => $match->status,
|
||||
'winner_id' => $match->winner_id,
|
||||
'winner_name' => $match->winner ? $match->winner->name : 'مساوی (Draw)',
|
||||
'players' => $match->players->map(function ($p) {
|
||||
return [
|
||||
'user_id' => $p->user_id,
|
||||
'name' => optional($p->user)->name,
|
||||
'final_score' => round($p->score, 2)
|
||||
];
|
||||
})
|
||||
], 'موفق', 'نتایج نهایی مسابقه دریافت شد.');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user