Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Pangolivia.API/Api/DTOs/QuestionScoresDto.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ public record PlayerQuestionScoresDto
public int UserId { get; init; }
public required string Username { get; init; }
public double Score { get; init; }
}
public int TotalScore { get; init; }
}
41 changes: 33 additions & 8 deletions Pangolivia.API/GameEngine/GameSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,17 @@ public class GameSession
public QuizModel Quiz { get; }

private int CurrentQuestionIndex = -1;
private DateTime QuestionStartTime;
public CancellationTokenSource? QuestionCts { get; set; }

private GameStatus Status { get; set; } = GameStatus.NotStarted;
public string? HostConnectionId { get; set; }
public readonly ConcurrentDictionary<int, Player> Players; // Made public for easier access from GameManager
// Scoring Constants
private const int QuestionTimeSeconds = 10;
private const int MaxScorePerQuestion = 1000;
private const int MinScorePerQuestion = 500;


public GameSession(int id, string name, int hostUserId, string hostUsername, QuizModel quiz) // Add hostUsername
{
Expand Down Expand Up @@ -134,6 +140,7 @@ public QuestionForPlayerDto AdvanceQuestion()

CurrentQuestionIndex++;
Status = GameStatus.ActiveQuestion;
QuestionStartTime = DateTime.UtcNow;

QuestionModel currentQuestion = questions.ElementAt(CurrentQuestionIndex);

Expand All @@ -145,7 +152,7 @@ public QuestionForPlayerDto AdvanceQuestion()
currentQuestion.Answer4,
};

// Fisher–Yates shuffle
// Fisher Yates shuffle
for (int i = answers.Count - 1; i > 0; i--)
{
int j = Random.Shared.Next(i + 1);
Expand Down Expand Up @@ -180,7 +187,12 @@ public void AnswerQuestionForPlayer(int playerId, string answer)
throw new InvalidOperationException("Player entry is null for this game session.");
}

player.AnswerToCurrentQuestion = answer;
// Only record the first answer from the player for the current question
if (player.AnswerToCurrentQuestion == null)
{
player.AnswerToCurrentQuestion = answer;
player.AnswerTime = DateTime.UtcNow;
}
}

/// <summary>
Expand Down Expand Up @@ -211,16 +223,28 @@ public QuestionScoresDto EndQuestionRound()

foreach (Player player in Players.Values)
{
int scoreIncrement = 0;
if (
int scoreIncrement = 0; // Default score is 0 for incorrect/no answer
bool isCorrect =
player.AnswerToCurrentQuestion != null
&& player.AnswerToCurrentQuestion.Equals(
correctAnswer,
StringComparison.OrdinalIgnoreCase
)
)
);

// Calculate score only if the answer is correct
if (isCorrect && player.AnswerTime.HasValue)
{
scoreIncrement = 1;
var timeTaken = (player.AnswerTime.Value - QuestionStartTime).TotalSeconds;

// Clamp timeTaken to be within the question duration to handle any latency issues
timeTaken = Math.Max(0, Math.Min(timeTaken, QuestionTimeSeconds));

// Linear score decay formula: Score = Max - (Range * (TimeElapsed / TotalTime))
double scoreRatio = timeTaken / QuestionTimeSeconds;
double scoreRange = MaxScorePerQuestion - MinScorePerQuestion;
double rawScore = MaxScorePerQuestion - (scoreRange * scoreRatio);

scoreIncrement = (int)Math.Round(rawScore);
player.AddPoints(scoreIncrement);
}

Expand All @@ -230,6 +254,7 @@ public QuestionScoresDto EndQuestionRound()
UserId = player.UserId,
Username = player.Username,
Score = scoreIncrement,
TotalScore = player.CurrentScore
}
);

Expand Down Expand Up @@ -278,4 +303,4 @@ enum GameStatus
Pending,
ActiveQuestion,
Ended,
}
}
1 change: 1 addition & 0 deletions Pangolivia.API/GameEngine/Player.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ public class Player
public string Username { get; }
public string ConnectionId { get; set; }
public string? AnswerToCurrentQuestion { get; set; }
public DateTime? AnswerTime { get; set; }
public int CurrentScore { get; private set; }

public Player(UserDto user, string connectionId)
Expand Down
2 changes: 1 addition & 1 deletion Pangolivia.Frontend/src/contexts/SignalRContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export interface QuestionForPlayer {
export interface RoundResults {
question: string
answer: string
playerScores: { userId: number; username: string; score: number }[]
playerScores: { userId: number; username: string; score: number; totalScore: number }[]
}

export interface FinalGameRecord {
Expand Down
63 changes: 57 additions & 6 deletions Pangolivia.Frontend/src/pages/Game/components/RoundResults.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { useMemo } from 'react';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { CheckCircle, XCircle } from 'lucide-react';
import type { RoundResults } from '@/contexts/SignalRContext';

Expand All @@ -8,23 +15,67 @@ interface RoundResultsProps {
}

export function RoundResults({ results, currentUserId }: RoundResultsProps) {
const myScoreThisRound = results.playerScores.find(p => p.userId === currentUserId)?.score ?? 0;
const myScoreThisRound = results.playerScores.find((p) => p.userId === currentUserId)?.score ?? 0;

const leaderboard = useMemo(() => {
// Defensively sort by totalScore, defaulting to 0 if undefined
return [...results.playerScores].sort((a, b) => (b.totalScore ?? 0) - (a.totalScore ?? 0));
}, [results]);

return (
<section className="flex min-h-screen items-center justify-center px-4 py-16">
<Card className="w-full max-w-2xl text-center">
<CardHeader>
<Card className="w-full max-w-2xl">
<CardHeader className="text-center">
{myScoreThisRound > 0 ? (
<CheckCircle className="mx-auto h-12 w-12 text-green-500" />
) : (
<XCircle className="mx-auto h-12 w-12 text-red-500" />
)}
<CardTitle className="text-3xl">{myScoreThisRound > 0 ? "Correct!" : "Incorrect!"}</CardTitle>
<CardTitle className="text-3xl">
{myScoreThisRound > 0 ? 'Correct!' : 'Incorrect!'}
</CardTitle>
<CardDescription>The correct answer was:</CardDescription>
<p className="text-xl font-bold">{results.answer}</p>
</CardHeader>
<CardContent>
<p className="font-semibold">Waiting for the next question...</p>
<div className="space-y-4">
<h3 className="text-center text-xl font-semibold">Leaderboard</h3>
<div className="space-y-2">
{leaderboard.map((player, index) => {
const roundScore = player.score ?? 0;
const totalScore = player.totalScore ?? 0;

return (
<div
key={player.userId}
className={`flex items-center justify-between rounded-lg border p-3 ${
player.userId === currentUserId
? 'border-primary ring-2 ring-primary/20'
: ''
}`}
>
<div className="flex items-center gap-3">
<span className="w-6 text-center font-bold text-muted-foreground">
{index + 1}
</span>
<p className="font-medium">{player.username}</p>
</div>
<div className="text-right">
<p className="font-bold">{totalScore.toLocaleString()}</p>
{roundScore > 0 && (
<p className="text-sm text-green-600">
+{roundScore.toLocaleString()}
</p>
)}
</div>
</div>
);
})}
</div>
</div>
<p className="mt-6 text-center font-semibold text-muted-foreground">
Waiting for the next question...
</p>
</CardContent>
</Card>
</section>
Expand Down
Loading