import { useNavigate } from '@solidjs/router' import { createEffect, createMemo, createSignal, onMount } from 'solid-js' import { AnswerInput, AttemptIndicator, GameCard, HintButton, ScoreDisplay, Timer, } from '@knowfoolery/ui-components' import Button from '@suid/material/Button' import Stack from '@suid/material/Stack' import Typography from '@suid/material/Typography' import { useTimer } from '../hooks/useTimer' import { leaderboardClient } from '../services/api' import { appendGameHistory, saveLastResult } from '../services/session' type QuizQuestion = { theme: string text: string answer: string hint: string } const QUESTION: QuizQuestion = { theme: 'Général', text: 'Quel est le plus petit nombre premier ?', answer: '2', hint: 'C’est le seul nombre premier pair.', } function normalizeAnswer(answer: string): string { return answer.trim().toLowerCase() } async function estimateLeaderboardPosition(score: number, durationSec: number): Promise { try { const rows = await leaderboardClient.top10() const sorted = [...rows].sort((a, b) => { if (b.score !== a.score) return b.score - a.score const durationA = typeof a.durationSec === 'number' ? a.durationSec : Number.MAX_SAFE_INTEGER const durationB = typeof b.durationSec === 'number' ? b.durationSec : Number.MAX_SAFE_INTEGER return durationA - durationB }) let rank = 1 for (const row of sorted) { const rowDuration = typeof row.durationSec === 'number' ? row.durationSec : Number.MAX_SAFE_INTEGER if (score > row.score) break if (score === row.score && durationSec < rowDuration) break rank += 1 } return rank <= 10 ? rank : null } catch { return null } } export default function GameRoute() { const navigate = useNavigate() const [question] = createSignal(QUESTION) const [answer, setAnswer] = createSignal('') const [attempts, setAttempts] = createSignal(0) const [hintUsed, setHintUsed] = createSignal(false) const [score, setScore] = createSignal(0) const [message, setMessage] = createSignal(null) const [finished, setFinished] = createSignal(false) const durationMs = 30 * 60 * 1000 const startedAt = Date.now() const { remainingMs, isExpired, start, stop } = useTimer(durationMs) onMount(() => start()) const attemptsLeft = createMemo(() => Math.max(0, 3 - attempts())) const answerLocked = createMemo(() => finished() || attemptsLeft() === 0 || isExpired()) const finalize = async (finalScore: number, answered: number, correct: number) => { if (finished()) return setFinished(true) stop() const elapsedSec = Math.max(1, Math.floor((Date.now() - startedAt) / 1000)) const durationSec = Math.min(Math.floor(durationMs / 1000), elapsedSec) const successRate = answered > 0 ? Math.round((correct / answered) * 100) : 0 const playerName = window.localStorage.getItem('kf.playerName') ?? 'Anonymous' const leaderboardPosition = await estimateLeaderboardPosition(finalScore, durationSec) const result = { playerName, finalScore, answered, correct, successRate, durationSec, leaderboardPosition, finishedAt: new Date().toISOString(), } saveLastResult(result) appendGameHistory(result) navigate('/results') } createEffect(() => { if (isExpired() && !finished()) { setMessage('Temps écoulé.') const answered = attempts() > 0 ? 1 : 0 void finalize(score(), answered, 0) } }) const submit = () => { if (answerLocked()) return const normalized = normalizeAnswer(answer()) if (!normalized) { setMessage('Saisis une réponse avant d’envoyer.') return } const isCorrect = normalized === normalizeAnswer(question().answer) || normalized === 'deux' const nextAttempts = attempts() + 1 setAttempts(nextAttempts) if (isCorrect) { const delta = hintUsed() ? 1 : 2 const nextScore = score() + delta setScore(nextScore) setMessage(`Bonne réponse (+${delta}).`) void finalize(nextScore, 1, 1) return } if (nextAttempts >= 3) { setMessage("Mauvaise réponse. Plus d'essais.") void finalize(score(), 1, 0) } else { setMessage('Mauvaise réponse.') } setAnswer('') } const confirmHint = () => { if (hintUsed() || attempts() > 0 || answerLocked()) return setHintUsed(true) setMessage(`Indice: ${question().hint}`) } return ( } scoreSlot={} answerSlot={ } attemptSlot={} actionsSlot={ 0 || answerLocked()} buttonLabel="Indice (score réduit)" confirmTitle="Confirmer l’indice" confirmMessage="Demander un indice réduit le score maximal de la question de 2 à 1 point. Continuer ?" confirmLabel="Oui, utiliser un indice" cancelLabel="Annuler" onConfirm={confirmHint} /> } feedbackSlot={ <> {message() && {message()}} {attemptsLeft() === 0 && Plus d'essais.} {isExpired() && Temps écoulé.} } /> ) }