Files
law-api/app/Http/Controllers/api/PvPQuizController.php
2026-08-20 16:21:03 +03:30

267 lines
9.5 KiB
PHP

<?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;
use Illuminate\Support\Str;
class PvPQuizController extends Controller
{
use BaseApiResponse;
/**
* STEP 1: Join the Matchmaking Queue
*/
public function findMatch(Request $request)
{
$userId = auth()->id();
$ticketId = $request->input('ticket_id') ?? 'req_' . Str::random(12);
// 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['user_id'] != $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['user_id'], '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);
// Prepare the payload for both players
$matchData = [
'match_id' => $match->id,
'status' => 'matched',
'questions' => $match->questions()->with('options')->get()->makeHidden(['solution_explanation'])->each(function($q) {
$q->options->each->makeHidden(['is_correct']);
})
];
// FIRE THE WEBSOCKET EVENT TO PLAYER A!
event(new \App\Events\MatchStarted($matchData, $waitingPlayer['ticket_id']));
// Return the same payload to Player B via standard HTTP response
$matchData['opponent_id'] = $waitingPlayer['user_id'];
return $this->success($matchData, 'موفق', 'حریف پیدا شد! بازی شروع می‌شود.');
}
// NO MATCH FOUND YET. Put Player A in the waiting queue and save their Ticket ID
Cache::put('pvp_waiting_player', [
'user_id' => $userId,
'ticket_id' => $ticketId
], 30);
return $this->success([
'status' => 'waiting',
'ticket_id' => $ticketId
], 'در حال جستجو', 'در حال یافتن حریف...');
}
/**
* 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
]);
// Prevent answering the same question twice!
$alreadyAnswered = $match->answers()
->where('user_id', auth()->id())
->where('question_id', $request->question_id)
->exists();
if ($alreadyAnswered) {
return $this->failed(null, 'خطا', 'شما قبلا به این سوال پاسخ داده‌اید.');
}
$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)
{
$players = $match->players()->orderByDesc('score')->get();
$player1 = $players->first();
$player2 = $players->last();
$winnerId = null;
if ($player1->score > $player2->score) {
$winnerId = $player1->user_id;
}
$match->update([
'status' => 'completed',
'ended_at' => now(),
'winner_id' => $winnerId
]);
// --- UPDATE GAMIFICATION STATS ---
$user1 = \App\Models\User::find($player1->user_id);
$user2 = \App\Models\User::find($player2->user_id);
$user1->increment('duels_played');
$user2->increment('duels_played');
// Give XP, Wins, and update "Win Duel" Quests
if ($winnerId) {
$winner = \App\Models\User::find($winnerId);
$winner->increment('duels_won');
$winner->increment('xp', 50); // Winner gets 50 XP
$duelQuest = $winner->activeQuests()
->whereHas('quest', function ($q) {
$q->where('quest_type', 'duel_win');
})->where('is_completed', false)->first();
if ($duelQuest) {
$duelQuest->increment('current_progress');
if ($duelQuest->current_progress >= $duelQuest->quest->required_amount) {
$duelQuest->update(['is_completed' => true]);
$winner->increment('xp', $duelQuest->quest->reward_xp);
}
}
}
// Loser/Draw gets participation XP
$loserId = $winnerId === $user1->id ? $user2->id : $user1->id;
\App\Models\User::find($loserId)->increment('xp', 10);
// Update "Answer Tests" Quest Progress for BOTH players
foreach ([$user1, $user2] as $player) {
$testQuest = $player->activeQuests()
->whereHas('quest', function ($q) {
$q->where('quest_type', 'answer_tests');
})->where('is_completed', false)->first();
if ($testQuest) {
$testQuest->increment('current_progress', 5); // They both answered 5 questions
if ($testQuest->current_progress >= $testQuest->quest->required_amount) {
$testQuest->update(['is_completed' => true]);
$player->increment('xp', $testQuest->quest->reward_xp);
}
}
}
// FIRE THE WEBSOCKET EVENT TO END MATCH!
event(new \App\Events\MatchEnded($match->id));
}
/**
* 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)
];
})
], 'موفق', 'نتایج نهایی مسابقه دریافت شد.');
}
}