Triviaverse / src /components /games /MathGame.tsx
Simeon Garratt
feat: remove Supabase, bundle questions locally, use JSON DB + API
ad111f4
Raw
History Blame Contribute Delete
5.08 kB
"use client";
import { useState, useEffect, useRef } from "react";
import { Calculator } from "lucide-react";
import { Button } from "@/components/ui/Button";
import { MiniGameResultsScreen } from "@/components/game/MiniGameResultsScreen";
import { MiniGameHeader } from "@/components/game/MiniGameHeader";
import { soundManager } from "@/lib/sound/manager";
const GAME_DURATION = 60;
export default function MathGame() {
const [isPlaying, setIsPlaying] = useState(false);
const [isGameOver, setIsGameOver] = useState(false);
const [timeLeft, setTimeLeft] = useState(GAME_DURATION);
const [score, setScore] = useState(0);
const [equation, setEquation] = useState({ text: "", answer: 0 });
const [inputValue, setInputValue] = useState("");
const inputRef = useRef<HTMLInputElement>(null);
const timerRef = useRef<NodeJS.Timeout | null>(null);
const generateEquation = () => {
const ops = ["+", "-", "*"];
const op = ops[Math.floor(Math.random() * ops.length)];
let a: number, b: number, text: string, answer: number;
const maxVal = 10 + Math.floor(score / 5) * 5;
if (op === "+") {
a = Math.floor(Math.random() * maxVal) + 1;
b = Math.floor(Math.random() * maxVal) + 1;
text = `${a} + ${b}`;
answer = a + b;
} else if (op === "-") {
a = Math.floor(Math.random() * maxVal) + 10;
b = Math.floor(Math.random() * a);
text = `${a} - ${b}`;
answer = a - b;
} else {
a = Math.floor(Math.random() * 12) + 2;
b = Math.floor(Math.random() * 12) + 2;
text = `${a} \u00d7 ${b}`;
answer = a * b;
}
setEquation({ text, answer });
};
const startGame = () => {
setIsPlaying(true);
setIsGameOver(false);
setScore(0);
setTimeLeft(GAME_DURATION);
setInputValue("");
generateEquation();
soundManager.play("streak5");
setTimeout(() => inputRef.current?.focus(), 100);
};
useEffect(() => {
if (!isPlaying || timeLeft <= 0) return;
timerRef.current = setTimeout(() => {
if (timeLeft <= 1) {
setIsPlaying(false);
setIsGameOver(true);
setTimeLeft(0);
soundManager.play("correct");
} else {
if (timeLeft <= 6) soundManager.play("tick");
setTimeLeft((prev) => prev - 1);
}
}, 1000);
return () => {
if (timerRef.current) clearTimeout(timerRef.current);
};
}, [isPlaying, timeLeft]);
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (!isPlaying) return;
const val = e.target.value;
if (!/^\d*$/.test(val)) return;
setInputValue(val);
if (parseInt(val) === equation.answer) {
soundManager.play("tick");
setScore((prev) => prev + 1);
setInputValue("");
generateEquation();
}
};
if (isGameOver) {
return (
<MiniGameResultsScreen
gameId="math"
score={score}
stats={[
{ label: "Equations Solved", value: `${score}` },
{
label: "Rate",
value: `${(score / (GAME_DURATION / 60)).toFixed(1)}/min`,
},
]}
onPlayAgain={startGame}
/>
);
}
return (
<div className="flex-1 flex flex-col w-full max-w-3xl mx-auto items-center gap-4 px-4">
<MiniGameHeader
title="Quantum Compute"
icon={Calculator}
accentColor="cyan"
stats={[
{
label: "Time",
value: `0:${timeLeft.toString().padStart(2, "0")}`,
},
{ label: "Score", value: `${score}` },
]}
/>
{!isPlaying ? (
<div className="flex flex-col items-center justify-center mt-20">
<Calculator className="w-24 h-24 text-cyan-400 mb-8 opacity-80 drop-shadow-[0_0_15px_rgba(34,211,238,0.5)]" />
<h2 className="text-3xl font-bold text-white mb-4 uppercase tracking-widest">
Core Calibration
</h2>
<p className="text-slate-400 mb-8 text-center max-w-md">
The hyperdrive is failing! Solve as many equations as possible in 60
seconds to stabilize the core.
</p>
<Button variant="secondary" size="lg" onClick={startGame}>
Initialize Sequence
</Button>
</div>
) : (
<div className="w-full flex flex-col items-center mt-20">
<div className="text-7xl md:text-9xl font-black font-mono text-white mb-12 drop-shadow-[0_0_20px_rgba(255,255,255,0.3)]">
{equation.text}
</div>
<input
ref={inputRef}
type="text"
inputMode="numeric"
value={inputValue}
onChange={handleInputChange}
autoFocus
className="w-full max-w-sm bg-black/40 border-2 rounded-2xl px-6 py-6 text-center text-white text-5xl font-mono focus:outline-none transition-all border-cyan-500/50 focus:border-cyan-400 focus:ring-4 focus:ring-cyan-500/20 shadow-[0_0_30px_rgba(34,211,238,0.1)]"
placeholder="?"
/>
</div>
)}
</div>
);
}