diff --git a/public/BulletMath.png b/public/BulletMath.png new file mode 100644 index 0000000..0844fb8 Binary files /dev/null and b/public/BulletMath.png differ diff --git a/src/app/game/bullet-math/page.module.css b/src/app/game/bullet-math/page.module.css new file mode 100644 index 0000000..bea724c --- /dev/null +++ b/src/app/game/bullet-math/page.module.css @@ -0,0 +1,185 @@ +.container { + display: flex; + flex-direction: column; + align-items: center; + gap: 16px; + padding: 24px; +} + +.controls { + display: flex; + gap: 12px; + justify-content: center; + flex-wrap: wrap; +} + +.statusBar { + display: flex; + gap: 24px; + font-weight: bold; + margin-top: 10px; +} + +.equationBox { + font-size: 36px; + font-weight: 700; + padding: 20px 32px; + border-radius: 12px; + border: 2px solid #ddd; + min-width: 360px; + display: flex; + justify-content: center; + align-items: center; + gap: 12px; +} + +.buttons { + display: flex; + gap: 16px; + margin-top: 10px; +} + +.btn { + padding: 12px 20px; + border: none; + border-radius: 10px; + cursor: pointer; + font-weight: 700; + font-size: 18px; +} + +.true { background: #16a34a; color: white; } +.false { background: #dc2626; color: white; } +.neutral { background: #334155; color: white; } + +.radioGroup { + display: flex; + gap: 8px; + align-items: center; +} + +.input { + font-size: 28px; + font-weight: 600; + text-align: center; + padding: 10px 16px; + margin-left: 12px; + width: 120px; + border: 2px solid #aaa; + border-radius: 10px; + outline: none; + transition: border-color 0.2s, box-shadow 0.2s, color 0.2s; +} + +.input:focus { + border-color: #2563eb; + box-shadow: 0 0 6px rgba(37, 99, 235, 0.5); +} + +.wrong { + color: #dc2626; + border-color: #dc2626; +} + +.menuButton { + margin-top: 12px; + background: linear-gradient(135deg, #3b82f6, #2563eb); + color: white; + border: none; + padding: 12px 24px; + font-size: 1.1rem; + font-weight: bold; + border-radius: 10px; + cursor: pointer; + transition: all 0.3s ease; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); +} + +.menuButton:hover { + background: linear-gradient(135deg, #2563eb, #1e40af); + transform: translateY(-2px); +} + +.overlay { + position: fixed; + top: 0; left: 0; + width: 100%; height: 100%; + background: rgba(0,0,0,0.7); + display: flex; align-items: center; justify-content: center; + z-index: 1000; +} + +.completionMessage { + background: white; + padding: 30px; + border-radius: 16px; + text-align: center; + max-width: 400px; + width: 90%; + box-shadow: 0 10px 40px rgba(0,0,0,0.3); +} + +.finalStats { + margin: 20px 0; + font-size: 1.2rem; +} + +.resetButton { + background: linear-gradient(135deg, #22c55e, #16a34a); + color: white; + border: none; + padding: 12px 24px; + font-size: 1rem; + font-weight: bold; + border-radius: 10px; + cursor: pointer; + margin-bottom: 12px; +} + +.resetButton:hover { + background: linear-gradient(135deg, #16a34a, #15803d); +} + +@media (max-width: 400px) { + .equationBox { + min-width: auto; + font-size: 24px; + padding: 12px 16px; + } + + .input { + width: 80px; + font-size: 20px; + } + + .btn { + font-size: 16px; + padding: 10px 14px; + } +} + +.hintBox { + margin-top: 20px; + padding: 16px; + border: 2px dashed #f59e0b; + border-radius: 12px; + background: #fffbea; + font-size: 16px; + max-width: 500px; + text-align: left; + line-height: 1.6; +} + +.hintBox h3 { + margin-bottom: 10px; + color: #b45309; +} + +.hintBox pre { + background: #fef3c7; + padding: 8px 12px; + border-radius: 8px; + font-family: monospace; + font-size: 14px; + overflow-x: auto; +} diff --git a/src/app/game/bullet-math/page.tsx b/src/app/game/bullet-math/page.tsx new file mode 100644 index 0000000..459d70a --- /dev/null +++ b/src/app/game/bullet-math/page.tsx @@ -0,0 +1,268 @@ +'use client' +import { useCallback, useEffect, useRef, useState } from 'react' +import { useRouter } from 'next/navigation' +import styles from './page.module.css' + +type Difficulty = 'amateur' | 'normal' | 'veteran' +type Op = '+' | '-' | '×' | '÷' + +type Equation = { + a: number + b: number + op: Op + correct: number +} + +const OPS: Op[] = ['+', '-', '×', '÷'] + +function randint(min: number, max: number) { + return Math.floor(Math.random() * (max - min + 1)) + min +} +function pick(arr: T[]): T { + return arr[randint(0, arr.length - 1)] +} + +function compute(a: number, b: number, op: Op): number { + switch (op) { + case '+': return a + b + case '-': return a - b + case '×': return a * b + case '÷': return Math.trunc(a / b) + } +} + +// Ensure division problems are clean +function makeDivisible(aMin: number, aMax: number, bMin: number, bMax: number) { + let b = randint(bMin, bMax) + if (b === 0) b = 1 + const k = randint(Math.max(1, Math.ceil(aMin / b)), Math.max(1, Math.floor(aMax / b))) + const a = b * k + return { a, b } +} + +// Dynamic equation generator +function generateEquation(diff: Difficulty): Equation { + const op = pick(OPS) + let a = 0, b = 0 + + if (diff === 'amateur') { + if (op === '÷') { + const { a: aa, b: bb } = makeDivisible(1, 81, 1, 9) + a = aa; b = bb + } else if (op === '×') { + a = randint(1, 9); b = randint(1, 9) + } else { + a = randint(1, 20); b = randint(1, 20) + } + } else if (diff === 'normal') { + if (op === '÷') { + const { a: aa, b: bb } = makeDivisible(10, 196, 2, 14) + a = aa; b = bb + } else if (op === '×') { + a = randint(6, 14); b = randint(6, 14) + } else { + a = randint(10, 99); b = randint(10, 99) + } + } else { + if (op === '×' && Math.random() < 0.5) { + a = randint(6, 12) + const tens = randint(10, 90) + const ones = randint(1, 9) + b = tens + ones + } else if (op === '×') { + const three = randint(100, 999) + const two = randint(10, 25) + if (Math.random() < 0.5) { a = three; b = two } else { a = two; b = three } + } else if (op === '÷') { + const bCand = randint(10, 25) + const k = randint(2, 30) + const aCand = bCand * k + const aMax = 999 + a = aCand > aMax ? bCand * randint(2, Math.floor(aMax / bCand)) : aCand + b = bCand + } else if (op === '+') { + a = randint(50, 999); b = randint(10, 250) + } else { + a = randint(50, 999); b = randint(10, Math.min(250, a)) + } + } + + return { a, b, op, correct: compute(a, b, op) } +} + +// Dynamic hint generator for Veteran mode +function generateHint(a: number, b: number, op: Op): string { + switch (op) { + case '+': + return `💡 Estimation tip: ${a} + ${b} ≈ ${Math.round(a / 100) * 100} + ${Math.round(b / 100) * 100}` + case '-': + return `💡 Compensation trick: ${a} - ${b} → Add ${(10 - (b % 10)) % 10} to both numbers for easier subtraction.` + case '×': + if (b > 9 && b < 100) { + const tens = Math.floor(b / 10) * 10 + const ones = b % 10 + return `💡 Break it down: ${a} × ${b} = (${a} × ${tens}) + (${a} × ${ones})` + } + return `💡 Split multiplication: (${a} × 10) + (${a} × (${b} - 10))` + case '÷': + return `💡 Think: what × ${b} ≈ ${a}? Start with ${Math.floor(a / b)} and adjust.` + default: + return '' + } +} + +export default function BulletMath() { + const [difficulty, setDifficulty] = useState('amateur') + const [running, setRunning] = useState(false) + const [timeLeft, setTimeLeft] = useState(120) + const [score, setScore] = useState(0) + const [attempted, setAttempted] = useState(0) + const [equation, setEquation] = useState(null) + const [userAnswer, setUserAnswer] = useState('') + const [isWrong, setIsWrong] = useState(false) + const [gameOver, setGameOver] = useState(false) + const [attemptedThisEquation, setAttemptedThisEquation] = useState(false) + const timerRef = useRef | null>(null) + const router = useRouter() + + const startGame = useCallback(() => { + setScore(0) + setAttempted(0) + setTimeLeft(120) + setRunning(true) + setEquation(generateEquation(difficulty)) + setUserAnswer('') + setIsWrong(false) + setGameOver(false) + setAttemptedThisEquation(false) + }, [difficulty]) + + useEffect(() => { + if (!running) return + timerRef.current = setInterval(() => { + setTimeLeft(t => { + if (t <= 1) { + if (timerRef.current) { + clearInterval(timerRef.current) + timerRef.current = null + } + setRunning(false) + setGameOver(true) + return 0 + } + return t - 1 + }) + }, 1000) + return () => { + if (timerRef.current) { + clearInterval(timerRef.current) + timerRef.current = null + } + } + }, [running]) + + const getPointsForDifficulty = (diff: Difficulty) => { + switch (diff) { + case 'amateur': return 1 + case 'normal': return 2 + case 'veteran': return 3 + } + } + + const onSubmitAnswer = () => { + if (!equation || !running) return + const input = userAnswer.trim() + if (!/^-?\d+$/.test(input)) return + + const parsed = Number(input) + + if (!attemptedThisEquation) { + setAttempted(n => n + 1) // count once per question + setAttemptedThisEquation(true) + } + + if (parsed === equation.correct) { + setScore(s => s + getPointsForDifficulty(difficulty)) + setEquation(generateEquation(difficulty)) + setUserAnswer('') + setIsWrong(false) + setAttemptedThisEquation(false) // reset for next question + } else { + setIsWrong(true) + } + } + + return ( +
+

Bullet Math

+ +
+
+ + + +
+
+ +
+ ⏳ {timeLeft}s + ⭐ {score} + 📊 {attempted} attempted +
+ +
+ {equation && running ? ( + <> + {equation.a} {equation.op} {equation.b} ={' '} + setUserAnswer(e.target.value)} + onKeyDown={(e) => { if (e.key === 'Enter') onSubmitAnswer() }} + className={`${styles.input} ${isWrong ? styles.wrong : ''}`} + aria-label="Your answer" + placeholder="?" + /> + + ) : ( + !gameOver && ( + + ) + )} +
+ + {difficulty === 'veteran' && running && equation && ( +
+

💡 Math Hack

+

{generateHint(equation.a, equation.b, equation.op)}

+
+ )} + + {gameOver && ( +
+
+

🎉 Time’s Up!

+
+

⭐ Score: {score}

+

📊 Questions Attempted: {attempted}

+
+ + +
+
+ )} +
+ ) +} diff --git a/src/app/game/page.tsx b/src/app/game/page.tsx index 057508d..4ad0d94 100644 --- a/src/app/game/page.tsx +++ b/src/app/game/page.tsx @@ -5,6 +5,8 @@ import TicTacToeImage from '/public/tic-tac-toe.svg' import SudokuImage from '/public/SudokuLogo.webp' import SchulteImage from '/public/SchulteImage.webp' import MemoryFlip from '/public/MemoryFlip.png' +import BulletMath from '/public/BulletMath.png' + const SelectGame = () => { return ( <> @@ -44,6 +46,13 @@ const SelectGame = () => { backgroundColor={'#760172'} textColor={'white'} /> + )