- {sortedPlayers.map((player) => (
-
-
-
{player.displayName}
-
+
+
+ {sortedPlayers.map((player, index) => (
+
))}
-
-
-
-
-
-
- {sortedPlayers.map((entry, index) => (
- |
- ))}
- (
-
- )}
- />
-
-
-
-
-
+
{isHost ? (
) : (
-
- Waiting for host...
+
+ Waiting for host...
)}
);
-};
\ No newline at end of file
+};
diff --git a/constants.ts b/constants.ts
index ecc3c83..476eff1 100644
--- a/constants.ts
+++ b/constants.ts
@@ -1,5 +1,5 @@
import { Triangle, Diamond, Circle, Square } from 'lucide-react';
-import type { GameConfig, Player } from './types';
+import type { GameConfig, Player, PointsBreakdown } from './types';
export const COLORS = {
red: 'bg-red-600',
@@ -48,33 +48,50 @@ interface PointsCalculationParams {
config: GameConfig;
}
-export const calculatePoints = (params: PointsCalculationParams): number => {
+export const calculatePointsWithBreakdown = (params: PointsCalculationParams): PointsBreakdown => {
const { isCorrect, timeLeftMs, questionTimeMs, streak, playerRank, isFirstCorrect, config } = params;
+ const breakdown: PointsBreakdown = {
+ basePoints: 0,
+ streakBonus: 0,
+ comebackBonus: 0,
+ firstCorrectBonus: 0,
+ penalty: 0,
+ total: 0,
+ };
+
if (!isCorrect) {
if (config.penaltyForWrongAnswer) {
- return -Math.round(POINTS_PER_QUESTION * (config.penaltyPercent / 100));
+ breakdown.penalty = Math.round(POINTS_PER_QUESTION * (config.penaltyPercent / 100));
+ breakdown.total = -breakdown.penalty;
}
- return 0;
+ return breakdown;
}
- let points = calculateBasePoints(timeLeftMs, questionTimeMs);
+ breakdown.basePoints = calculateBasePoints(timeLeftMs, questionTimeMs);
+ let pointsAfterStreak = breakdown.basePoints;
if (config.streakBonusEnabled && streak >= config.streakThreshold) {
- const streakBonus = streak - config.streakThreshold;
- const multiplier = config.streakMultiplier + (streakBonus * (config.streakMultiplier - 1));
- points = Math.round(points * multiplier);
+ const streakCount = streak - config.streakThreshold;
+ const multiplier = config.streakMultiplier + (streakCount * (config.streakMultiplier - 1));
+ pointsAfterStreak = Math.round(breakdown.basePoints * multiplier);
+ breakdown.streakBonus = pointsAfterStreak - breakdown.basePoints;
}
if (config.comebackBonusEnabled && playerRank > 3) {
- points += config.comebackBonusPoints;
+ breakdown.comebackBonus = config.comebackBonusPoints;
}
if (config.firstCorrectBonusEnabled && isFirstCorrect) {
- points += config.firstCorrectBonusPoints;
+ breakdown.firstCorrectBonus = config.firstCorrectBonusPoints;
}
- return points;
+ breakdown.total = pointsAfterStreak + breakdown.comebackBonus + breakdown.firstCorrectBonus;
+ return breakdown;
+};
+
+export const calculatePoints = (params: PointsCalculationParams): number => {
+ return calculatePointsWithBreakdown(params).total;
};
export const getPlayerRank = (playerId: string, players: Player[]): number => {
diff --git a/hooks/useGame.ts b/hooks/useGame.ts
index e21d00b..97b6faa 100644
--- a/hooks/useGame.ts
+++ b/hooks/useGame.ts
@@ -1,7 +1,7 @@
import { useState, useEffect, useRef, useCallback } from 'react';
-import { Quiz, Player, GameState, GameRole, NetworkMessage, AnswerOption, Question, GenerateQuizOptions, ProcessedDocument, GameConfig, DEFAULT_GAME_CONFIG } from '../types';
+import { Quiz, Player, GameState, GameRole, NetworkMessage, AnswerOption, Question, GenerateQuizOptions, ProcessedDocument, GameConfig, DEFAULT_GAME_CONFIG, PointsBreakdown } from '../types';
import { generateQuiz } from '../services/geminiService';
-import { QUESTION_TIME, QUESTION_TIME_MS, PLAYER_COLORS, calculatePoints, getPlayerRank } from '../constants';
+import { QUESTION_TIME, QUESTION_TIME_MS, PLAYER_COLORS, calculatePointsWithBreakdown, getPlayerRank } from '../constants';
import { Peer, DataConnection } from 'peerjs';
const BACKEND_URL = import.meta.env.VITE_BACKEND_URL || 'http://localhost:3001';
@@ -158,8 +158,10 @@ export const useGame = () => {
id: 'host',
name: 'Host',
score: 0,
+ previousScore: 0,
streak: 0,
lastAnswerCorrect: null,
+ pointsBreakdown: null,
isBot: false,
avatarSeed: Math.random(),
color: PLAYER_COLORS[0]
@@ -197,8 +199,10 @@ export const useGame = () => {
id: conn.peer,
name: data.payload.name,
score: 0,
+ previousScore: 0,
streak: 0,
lastAnswerCorrect: null,
+ pointsBreakdown: null,
isBot: false,
avatarSeed: Math.random(),
color: PLAYER_COLORS[colorIndex]
@@ -224,7 +228,7 @@ export const useGame = () => {
const newStreak = isCorrect ? currentPlayer.streak + 1 : 0;
const playerRank = getPlayerRank(playerId, playersRef.current);
- const points = calculatePoints({
+ const breakdown = calculatePointsWithBreakdown({
isCorrect,
timeLeftMs: timeLeftRef.current,
questionTimeMs: QUESTION_TIME_MS,
@@ -233,14 +237,14 @@ export const useGame = () => {
isFirstCorrect,
config: gameConfigRef.current,
});
- const newScore = Math.max(0, currentPlayer.score + points);
+ const newScore = Math.max(0, currentPlayer.score + breakdown.total);
setPlayers(prev => prev.map(p => {
if (p.id !== playerId) return p;
- return { ...p, score: newScore, streak: newStreak, lastAnswerCorrect: isCorrect };
+ return { ...p, score: newScore, previousScore: p.score, streak: newStreak, lastAnswerCorrect: isCorrect, pointsBreakdown: breakdown };
}));
- conn.send({ type: 'RESULT', payload: { isCorrect, scoreAdded: points, newScore } });
+ conn.send({ type: 'RESULT', payload: { isCorrect, scoreAdded: breakdown.total, newScore, breakdown } });
}
};
@@ -284,7 +288,7 @@ export const useGame = () => {
setSelectedOption(null);
setTimeLeft(QUESTION_TIME_MS);
setFirstCorrectPlayerId(null);
- setPlayers(prev => prev.map(p => ({ ...p, lastAnswerCorrect: null })));
+ setPlayers(prev => prev.map(p => ({ ...p, lastAnswerCorrect: null, pointsBreakdown: null })));
const currentQuiz = quizRef.current;
const currentIndex = currentQuestionIndexRef.current;
@@ -469,7 +473,7 @@ export const useGame = () => {
const playerRank = getPlayerRank('host', playersRef.current);
- const points = calculatePoints({
+ const breakdown = calculatePointsWithBreakdown({
isCorrect,
timeLeftMs: timeLeftRef.current,
questionTimeMs: QUESTION_TIME_MS,
@@ -479,14 +483,14 @@ export const useGame = () => {
config: gameConfigRef.current,
});
- setLastPointsEarned(points);
- const newScore = Math.max(0, (hostPlayer?.score || 0) + points);
+ setLastPointsEarned(breakdown.total);
+ const newScore = Math.max(0, (hostPlayer?.score || 0) + breakdown.total);
setCurrentPlayerScore(newScore);
setCurrentStreak(newStreak);
setPlayers(prev => prev.map(p => {
if (p.id !== 'host') return p;
- return { ...p, score: newScore, streak: newStreak, lastAnswerCorrect: isCorrect };
+ return { ...p, score: newScore, previousScore: p.score, streak: newStreak, lastAnswerCorrect: isCorrect, pointsBreakdown: breakdown };
}));
} else {
const option = arg as AnswerOption;
diff --git a/types.ts b/types.ts
index 0689dbc..0733de7 100644
--- a/types.ts
+++ b/types.ts
@@ -96,12 +96,23 @@ export interface GenerateQuizOptions {
documents?: ProcessedDocument[];
}
+export interface PointsBreakdown {
+ basePoints: number;
+ streakBonus: number;
+ comebackBonus: number;
+ firstCorrectBonus: number;
+ penalty: number;
+ total: number;
+}
+
export interface Player {
id: string;
name: string;
score: number;
+ previousScore: number;
streak: number;
lastAnswerCorrect: boolean | null;
+ pointsBreakdown: PointsBreakdown | null;
isBot: boolean;
avatarSeed: number;
color: string;
@@ -126,7 +137,7 @@ export type NetworkMessage =
}
}
| { type: 'ANSWER'; payload: { playerId: string; isCorrect: boolean } }
- | { type: 'RESULT'; payload: { isCorrect: boolean; scoreAdded: number; newScore: number } }
+ | { type: 'RESULT'; payload: { isCorrect: boolean; scoreAdded: number; newScore: number; breakdown: PointsBreakdown } }
| { type: 'TIME_SYNC'; payload: { timeLeft: number } }
| { type: 'TIME_UP'; payload: {} }
| { type: 'SHOW_SCOREBOARD'; payload: { players: Player[] } }