113 lines
3.6 KiB
TypeScript
113 lines
3.6 KiB
TypeScript
import { Triangle, Diamond, Circle, Square } from 'lucide-react';
|
|
import type { GameConfig, Player, PointsBreakdown } from './types';
|
|
|
|
export const COLORS = {
|
|
red: 'bg-red-600',
|
|
blue: 'bg-blue-600',
|
|
yellow: 'bg-yellow-600',
|
|
green: 'bg-green-600',
|
|
purple: 'bg-theme-primary',
|
|
purpleLight: 'bg-theme-primary/70',
|
|
};
|
|
|
|
export const SHAPES = {
|
|
triangle: Triangle,
|
|
diamond: Diamond,
|
|
circle: Circle,
|
|
square: Square,
|
|
};
|
|
|
|
export const BOT_NAMES = [
|
|
"QuizWiz", "FastFinger", "Brainiac", "KnowItAll", "Guesser",
|
|
"LuckyStrike", "OwlFan", "Kahootie", "ZigZag", "Pixel"
|
|
];
|
|
|
|
export const QUESTION_TIME = 20; // seconds
|
|
export const QUESTION_TIME_MS = 20000; // milliseconds
|
|
export const POINTS_PER_QUESTION = 1000;
|
|
|
|
export const calculateBasePoints = (timeLeftMs: number, questionTimeMs: number, maxPoints: number = POINTS_PER_QUESTION): number => {
|
|
const responseTimeMs = questionTimeMs - timeLeftMs;
|
|
const responseTimeSec = responseTimeMs / 1000;
|
|
const questionTimeSec = questionTimeMs / 1000;
|
|
|
|
if (responseTimeSec < 0.5) {
|
|
return maxPoints;
|
|
}
|
|
|
|
return Math.round((1 - (responseTimeSec / questionTimeSec) / 2) * maxPoints);
|
|
};
|
|
|
|
interface PointsCalculationParams {
|
|
isCorrect: boolean;
|
|
timeLeftMs: number;
|
|
questionTimeMs: number;
|
|
streak: number;
|
|
playerRank: number;
|
|
isFirstCorrect: boolean;
|
|
config: GameConfig;
|
|
currentQuestionIndex?: 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) {
|
|
breakdown.penalty = Math.round(POINTS_PER_QUESTION * (config.penaltyPercent / 100));
|
|
breakdown.total = -breakdown.penalty;
|
|
}
|
|
return breakdown;
|
|
}
|
|
|
|
breakdown.basePoints = calculateBasePoints(timeLeftMs, questionTimeMs);
|
|
let pointsAfterStreak = breakdown.basePoints;
|
|
|
|
if (config.streakBonusEnabled && streak >= config.streakThreshold) {
|
|
const streakCount = streak - config.streakThreshold;
|
|
const multiplier = config.streakMultiplier + (streakCount * (config.streakMultiplier - 1));
|
|
pointsAfterStreak = Math.round(breakdown.basePoints * multiplier);
|
|
breakdown.streakBonus = pointsAfterStreak - breakdown.basePoints;
|
|
}
|
|
|
|
const isFirstQuestion = params.currentQuestionIndex === 0 || params.currentQuestionIndex === undefined;
|
|
if (config.comebackBonusEnabled && playerRank > 3 && !isFirstQuestion) {
|
|
breakdown.comebackBonus = config.comebackBonusPoints;
|
|
}
|
|
|
|
if (config.firstCorrectBonusEnabled && isFirstCorrect) {
|
|
breakdown.firstCorrectBonus = config.firstCorrectBonusPoints;
|
|
}
|
|
|
|
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 => {
|
|
const sorted = [...players].sort((a, b) => b.score - a.score);
|
|
return sorted.findIndex(p => p.id === playerId) + 1;
|
|
};
|
|
|
|
const GOLDEN_RATIO = 0.618033988749895;
|
|
|
|
export const generatePlayerColor = (index: number): string => {
|
|
const hue = ((index * GOLDEN_RATIO) % 1) * 360;
|
|
const saturation = 70 + (index % 3) * 10;
|
|
const lightness = 45 + (index % 2) * 10;
|
|
return `hsl(${Math.round(hue)}, ${saturation}%, ${lightness}%)`;
|
|
};
|
|
|
|
export const PLAYER_COLORS = Array.from({ length: 50 }, (_, i) => generatePlayerColor(i));
|