Add new brain game: Bullet Math #115
Conversation
|
@aleenaharoldpeter is attempting to deploy a commit to the ajaynegi45's projects Team on Vercel. A member of the Team first needs to authorize it. |
Summary by CodeRabbit
WalkthroughAdds a new Bullet Math client page and CSS module implementing a 120s timed arithmetic game (three difficulties, + − × ÷ with integer-safe division), scoring, veteran hints, input/submit flow, end-of-game modal, and a new game card on the game selection page. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor User
participant Page as BulletMath Page
participant Timer as Countdown Interval
participant Router as App Router
User->>Page: Open /game/bullet-math
Page-->>User: Show difficulty options, Start button
User->>Page: Click Start
Page->>Timer: start 120s interval
Page->>Page: generateEquation(), focus input
loop while time > 0
User->>Page: Type answer + Enter
alt correct
Page->>Page: increment score, attempts, generate next equation
Page-->>User: show next equation
else incorrect
Page->>Page: mark input wrong, count attempt
Page-->>User: show wrong state
end
opt veteran mode
Page-->>User: display hint
end
end
Timer-->>Page: time expired
Page->>Timer: clear interval
Page-->>User: show overlay (final stats, Replay/Home)
User->>Router: Replay or Navigate Home
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related issues
Poem
✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
👋 Hello @aleenaharoldpeter! Thanks for contributing to Old-School-Game – a curated collection of classic, brain-training games that boost memory, focus, creativity, and long-term brain health 🎯🧠. In today's world full of short-form distractions, this project helps rebuild deep focus and mental endurance through thoughtful gameplay. ✅ Important Things To Do:
✅ Your pull request has been submitted successfully, and a maintainer will review it soon. |
There was a problem hiding this comment.
Actionable comments posted: 7
📜 Review details
Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
⛔ Files ignored due to path filters (1)
public/BulletMath.pngis excluded by!**/*.png
📒 Files selected for processing (3)
src/app/game/bullet-math/page.module.css(1 hunks)src/app/game/bullet-math/page.tsx(1 hunks)src/app/game/page.tsx(2 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
src/app/game/page.tsx (1)
src/app/game/bullet-math/page.tsx (1)
BulletMath(116-274)
src/app/game/bullet-math/page.tsx (1)
src/app/game/schulte-table/page.tsx (2)
numbers(14-185)startNewGame(45-54)
🪛 Biome (2.1.2)
src/app/game/bullet-math/page.tsx
[error] 238-238: Avoid the autoFocus attribute.
Unsafe fix: Remove the autoFocus attribute.
(lint/a11y/noAutofocus)
[error] 243-243: Avoid the autoFocus attribute.
Unsafe fix: Remove the autoFocus attribute.
(lint/a11y/noAutofocus)
🔇 Additional comments (3)
src/app/game/page.tsx (2)
49-49: Card integration looks good.Props match the existing pattern; link resolves to
/game/bullet-math.
8-8: Confirm and correct BulletMath import path
public/BulletMath.png exists (case-matched). Change the import toimport BulletMath from "/BulletMath.png"and verify that your Card component’s props accept a StaticImageData import (otherwise pass the URL string).
src/app/game/bullet-math/page.tsx (1)
176-197: Attempt counter behavior: confirm intended semantics.You count “attempted” once per equation (first try only). If the goal is “questions answered” (regardless of multiple wrong tries), this is correct. If you meant “number of submissions,” this undercounts.
| .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; | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick (assertive)
Responsive min-width could still overflow on very narrow devices.
You already adjust under 400px; consider using min(360px, 100%) to avoid any overflow without the media query dependency.
-.equationBox {
+.equationBox {
font-size: 36px;
@@
- min-width: 360px;
+ min-width: min(360px, 100%);Also applies to: 143-159
🤖 Prompt for AI Agents
In src/app/game/bullet-math/page.module.css around lines 23 to 34 (and similarly
apply the same change to lines 143-159), the fixed min-width: 360px can still
cause horizontal overflow on very narrow screens; replace the hard min-width
with a responsive constraint like min-width: min(360px, 100%) (or use width:
min(360px, 100%)) so the element never exceeds the viewport width while
preserving the intended minimum on larger screens.
| .true { background: #16a34a; color: white; } | ||
| .false { background: #dc2626; color: white; } | ||
| .neutral { background: #334155; color: white; } | ||
|
|
There was a problem hiding this comment.
🧹 Nitpick (assertive)
Unused variants?
If .true/.false aren’t used, drop them to reduce CSS surface. Keep only what’s referenced in JSX.
🤖 Prompt for AI Agents
In src/app/game/bullet-math/page.module.css around lines 51 to 54, the CSS
defines .true and .false classes that may be unused; remove those class rules if
they are not referenced in any JSX/TSX to reduce CSS surface, or alternatively
verify and rename/replace them with the actual class names used in components
(or convert to a single reusable utility class) and update the JSX to reference
the kept class; ensure you run a quick grep/IDE search for ".true" and ".false"
usages before deleting to avoid breaking references.
| .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); | ||
| } | ||
|
|
There was a problem hiding this comment.
🧹 Nitpick (assertive)
Modal styles are solid; consider motion-reduction for hover animations.
Respect users who prefer reduced motion by disabling hover transforms.
@media (max-width: 400px) {
.equationBox {
@@
}
}
+
+@media (prefers-reduced-motion: reduce) {
+ .menuButton:hover,
+ .resetButton:hover {
+ transform: none;
+ }
+}Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In src/app/game/bullet-math/page.module.css around lines 103-121, add a
prefers-reduced-motion rule so users who prefer reduced motion won't see
hover/animation transforms: create an @media (prefers-reduced-motion: reduce)
block that sets transition: none; animation: none; and transform: none on
.overlay and .completionMessage (and any related hover/focus selectors used
elsewhere), so hover transforms and animations are disabled for those users.
There was a problem hiding this comment.
Actionable comments posted: 4
♻️ Duplicate comments (3)
src/app/game/bullet-math/page.tsx (3)
25-36: Exhaustive switch on Op — nice and safe.The default+never pattern makes future Op additions fail fast at compile-time. Good guardrail.
163-174: Managed focus looks good.Nice job replacing autoFocus with refs/effects and moving focus to Start/Play Again appropriately.
Also applies to: 241-247, 266-273
220-224: Polish: make status updates fully screen-reader friendly.Add
role="status"andaria-atomic="true"so updates are announced cleanly.- <div className={styles.statusBar} aria-live="polite"> + <div className={styles.statusBar} role="status" aria-live="polite" aria-atomic="true">
📜 Review details
Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (1)
src/app/game/bullet-math/page.tsx(1 hunks)
| const timerRef = useRef<ReturnType<typeof setInterval> | null>(null) | ||
| const inputRef = useRef<HTMLInputElement>(null) |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Remove unsafe casts when clearing the timer; use window.setInterval/clearInterval and null the ref.
The as unknown as number casts are brittle. Since this is a client component, prefer window.setInterval/window.clearInterval, and always null the ref after clearing to prevent double-clears or leaks.
Apply this diff:
- const timerRef = useRef<ReturnType<typeof setInterval> | null>(null)
+ const timerRef = useRef<number | null>(null)
@@
- timerRef.current = setInterval(() => {
+ timerRef.current = window.setInterval(() => {
setTimeLeft(t => {
if (t <= 1) {
- clearInterval(timerRef.current as unknown as number)
+ if (timerRef.current != null) {
+ window.clearInterval(timerRef.current)
+ timerRef.current = null
+ }
setRunning(false)
setGameOver(true)
return 0
}
return t - 1
})
}, 1000)
return () => {
- if (timerRef.current) clearInterval(timerRef.current as unknown as number)
+ if (timerRef.current != null) {
+ window.clearInterval(timerRef.current)
+ timerRef.current = null
+ }
}Also applies to: 147-161
🤖 Prompt for AI Agents
In src/app/game/bullet-math/page.tsx around lines 128-129 and also affecting
147-161, replace the unsafe casts when storing/clearing intervals by using
browser timer types and window helpers: change the timer ref to hold number |
null (e.g. useRef<number | null>(null)), use window.setInterval to assign a
numeric id to timerRef.current, use window.clearInterval(timerRef.current) when
clearing (guarding for non-null), and then set timerRef.current = null after
clearing to avoid double-clears and leaks; remove any "as unknown as number"
casts.
| <div className={styles.controls}> | ||
| <div className={styles.radioGroup}> | ||
| <label> | ||
| <input type="radio" checked={difficulty === 'amateur'} onChange={() => setDifficulty('amateur')} disabled={running}/> Amateur | ||
| </label> | ||
| <label> | ||
| <input type="radio" checked={difficulty === 'normal'} onChange={() => setDifficulty('normal')} disabled={running}/> Normal | ||
| </label> | ||
| <label> | ||
| <input type="radio" checked={difficulty === 'veteran'} onChange={() => setDifficulty('veteran')} disabled={running}/> Veteran | ||
| </label> | ||
| </div> |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Group radios with a shared name and mark the container as a radiogroup.
Without a name, screen readers may not treat these as one group. Adding role="radiogroup" and a label improves a11y and keyboard UX.
- <div className={styles.controls}>
- <div className={styles.radioGroup}>
+ <div className={styles.controls}>
+ <div className={styles.radioGroup} role="radiogroup" aria-label="Difficulty">
<label>
- <input type="radio" checked={difficulty === 'amateur'} onChange={() => setDifficulty('amateur')} disabled={running}/> Amateur
+ <input name="difficulty" type="radio" checked={difficulty === 'amateur'} onChange={() => setDifficulty('amateur')} disabled={running}/> Amateur
</label>
<label>
- <input type="radio" checked={difficulty === 'normal'} onChange={() => setDifficulty('normal')} disabled={running}/> Normal
+ <input name="difficulty" type="radio" checked={difficulty === 'normal'} onChange={() => setDifficulty('normal')} disabled={running}/> Normal
</label>
<label>
- <input type="radio" checked={difficulty === 'veteran'} onChange={() => setDifficulty('veteran')} disabled={running}/> Veteran
+ <input name="difficulty" type="radio" checked={difficulty === 'veteran'} onChange={() => setDifficulty('veteran')} disabled={running}/> Veteran
</label>
</div>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <div className={styles.controls}> | |
| <div className={styles.radioGroup}> | |
| <label> | |
| <input type="radio" checked={difficulty === 'amateur'} onChange={() => setDifficulty('amateur')} disabled={running}/> Amateur | |
| </label> | |
| <label> | |
| <input type="radio" checked={difficulty === 'normal'} onChange={() => setDifficulty('normal')} disabled={running}/> Normal | |
| </label> | |
| <label> | |
| <input type="radio" checked={difficulty === 'veteran'} onChange={() => setDifficulty('veteran')} disabled={running}/> Veteran | |
| </label> | |
| </div> | |
| <div className={styles.controls}> | |
| <div className={styles.radioGroup} role="radiogroup" aria-label="Difficulty"> | |
| <label> | |
| <input | |
| name="difficulty" | |
| type="radio" | |
| checked={difficulty === 'amateur'} | |
| onChange={() => setDifficulty('amateur')} | |
| disabled={running} | |
| /> Amateur | |
| </label> | |
| <label> | |
| <input | |
| name="difficulty" | |
| type="radio" | |
| checked={difficulty === 'normal'} | |
| onChange={() => setDifficulty('normal')} | |
| disabled={running} | |
| /> Normal | |
| </label> | |
| <label> | |
| <input | |
| name="difficulty" | |
| type="radio" | |
| checked={difficulty === 'veteran'} | |
| onChange={() => setDifficulty('veteran')} | |
| disabled={running} | |
| /> Veteran | |
| </label> | |
| </div> |
🤖 Prompt for AI Agents
In src/app/game/bullet-math/page.tsx around lines 206 to 217, the three radio
inputs are missing a shared name and the container lacks radiogroup semantics;
update the container to include role="radiogroup" and an accessible label
(either aria-label or aria-labelledby) and give each input the same name value
(e.g., name="difficulty") so assistive tech and keyboard navigation treat them
as one group; also ensure each label is associated with its input via for/id or
by wrapping as currently done so association remains intact.
| type="text" | ||
| value={userAnswer} | ||
| onChange={(e) => setUserAnswer(e.target.value)} | ||
| onKeyDown={(e) => { if (e.key === 'Enter') onSubmitAnswer() }} | ||
| className={`${styles.input} ${isWrong ? styles.wrong : ''}`} | ||
| aria-label="Your answer" | ||
| placeholder="?" | ||
| ref={inputRef} | ||
| /> |
There was a problem hiding this comment.
🧹 Nitpick (assertive)
Clear the red error state as soon as the user edits.
Resetting isWrong on input change gives instant feedback and reduces distraction.
- onChange={(e) => setUserAnswer(e.target.value)}
+ onChange={(e) => {
+ setUserAnswer(e.target.value)
+ if (isWrong) setIsWrong(false)
+ }}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| type="text" | |
| value={userAnswer} | |
| onChange={(e) => setUserAnswer(e.target.value)} | |
| onKeyDown={(e) => { if (e.key === 'Enter') onSubmitAnswer() }} | |
| className={`${styles.input} ${isWrong ? styles.wrong : ''}`} | |
| aria-label="Your answer" | |
| placeholder="?" | |
| ref={inputRef} | |
| /> | |
| type="text" | |
| value={userAnswer} | |
| onChange={(e) => { | |
| setUserAnswer(e.target.value) | |
| if (isWrong) setIsWrong(false) | |
| }} | |
| onKeyDown={(e) => { if (e.key === 'Enter') onSubmitAnswer() }} | |
| className={`${styles.input} ${isWrong ? styles.wrong : ''}`} | |
| aria-label="Your answer" | |
| placeholder="?" | |
| ref={inputRef} | |
| /> |
🤖 Prompt for AI Agents
In src/app/game/bullet-math/page.tsx around lines 231 to 239, the input change
handler should clear the red error state immediately; update the onChange
callback to call setUserAnswer(e.target.value) and also setIsWrong(false) (or
reset the wrong flag) when the user types so the red styling is removed as soon
as they edit, preserving the existing onKeyDown and other props.
| {gameOver && ( | ||
| <div className={styles.overlay} role="dialog" aria-modal="true" aria-labelledby="times-up-heading"> | ||
| <div className={styles.completionMessage}> | ||
| <h2 id="times-up-heading">🎉 Time’s Up!</h2> | ||
| <div className={styles.finalStats}> | ||
| <p>⭐ Score: {score}</p> | ||
| <p>📊 Questions Attempted: {attempted}</p> | ||
| </div> | ||
| <button type="button" className={styles.resetButton} onClick={startGame} ref={resetRef}> | ||
| 🔄 Play Again | ||
| </button> | ||
| <button type="button" className={styles.menuButton} onClick={() => router.push('/')}> | ||
| 🏠 Back to Homepage | ||
| </button> | ||
| </div> | ||
| </div> | ||
| )} |
There was a problem hiding this comment.
🧹 Nitpick (assertive)
Optional: trap focus inside the end-game dialog.
Right now, Tab can escape the modal. Consider a small focus trap (e.g., focus-trap-react) or sentinel nodes to keep keyboard focus within the dialog until it closes.
Happy to provide a minimal trap snippet if you want it inline without a dependency.
🤖 Prompt for AI Agents
In src/app/game/bullet-math/page.tsx around lines 258-274 the end-game dialog
renders but does not trap keyboard focus, allowing Tab to escape the modal; add
a focus trap so keyboard users remain inside while gameOver is true — either
install and wrap the dialog with a light focus-trap-react <FocusTrap> (activate
only when gameOver) or implement sentinel nodes and keydown handlers: on open
move focus to the first focusable control (reset button), handle Tab and
Shift+Tab to cycle between focusable elements, and restore focus when the dialog
closes; ensure the trap is mounted only for the modal and that focusable
elements have refs so the trap can manage focus.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Actionable comments posted: 6
♻️ Duplicate comments (8)
src/app/game/bullet-math/page.module.css (4)
23-34: Prevent overflow on small screens: use a responsive width cap.Replace fixed min-width with a responsive cap so it never overflows narrow devices.
.equationBox { font-size: 36px; font-weight: 700; padding: 20px 32px; border-radius: 12px; border: 2px solid #ddd; - min-width: 360px; + width: min(360px, 100%); display: flex; justify-content: center; align-items: center; gap: 12px; }
143-149: Media query override no longer needed if you adopt width:min(360px,100%).If you take the change above, this override becomes redundant and can be removed.
@media (max-width: 400px) { .equationBox { - min-width: auto; font-size: 24px; padding: 12px 16px; }
98-101: Respect reduced-motion preferences for hover effects.Disable transforms/transitions for users who prefer reduced motion.
.menuButton:hover { background: linear-gradient(135deg, #2563eb, #1e40af); transform: translateY(-2px); } + +@media (prefers-reduced-motion: reduce) { + .menuButton, + .resetButton { + transition: none; + } + .menuButton:hover, + .resetButton:hover { + transform: none; + } +}Also applies to: 127-141
51-54: Remove unused variants to slim CSS.
.true/.falsedon’t appear to be used in the TSX. If confirmed unused, delete them.#!/bin/bash # Verify unused CSS module class usages rg -nP "styles\.(true|false)\b|['\"](true|false)['\"]" --type=ts --type=tsx -C1src/app/game/bullet-math/page.tsx (4)
129-161: Safer timer typing and cleanup; remove brittle casts and always null the ref.Use
window.setInterval/clearIntervalwith a numeric id and null the ref after clearing to avoid double-clears/leaks.- const timerRef = useRef<ReturnType<typeof setInterval> | null>(null) + const timerRef = useRef<number | null>(null) @@ - timerRef.current = setInterval(() => { + timerRef.current = window.setInterval(() => { setTimeLeft(t => { if (t <= 1) { - clearInterval(timerRef.current as unknown as number) + if (timerRef.current != null) { + window.clearInterval(timerRef.current) + timerRef.current = null + } setRunning(false) setGameOver(true) return 0 } return t - 1 }) }, 1000) return () => { - if (timerRef.current) clearInterval(timerRef.current as unknown as number) + if (timerRef.current != null) { + window.clearInterval(timerRef.current) + timerRef.current = null + } }
231-244: Add radiogroup semantics and a shared name for radios.Improves screen reader grouping and keyboard UX.
- <div className={styles.controls}> - <div className={styles.radioGroup}> + <div className={styles.controls}> + <div className={styles.radioGroup} role="radiogroup" aria-label="Difficulty"> <label> - <input type="radio" checked={difficulty === 'amateur'} onChange={() => setDifficulty('amateur')} disabled={running}/> Amateur + <input name="difficulty" type="radio" checked={difficulty === 'amateur'} onChange={() => setDifficulty('amateur')} disabled={running}/> Amateur </label> <label> - <input type="radio" checked={difficulty === 'normal'} onChange={() => setDifficulty('normal')} disabled={running}/> Normal + <input name="difficulty" type="radio" checked={difficulty === 'normal'} onChange={() => setDifficulty('normal')} disabled={running}/> Normal </label> <label> - <input type="radio" checked={difficulty === 'veteran'} onChange={() => setDifficulty('veteran')} disabled={running}/> Veteran + <input name="difficulty" type="radio" checked={difficulty === 'veteran'} onChange={() => setDifficulty('veteran')} disabled={running}/> Veteran </label> </div>
257-266: Better mobile keypad + clear error state on edit.Surface a numeric keypad and remove the red state as soon as the user types.
<input type="text" value={userAnswer} - onChange={(e) => setUserAnswer(e.target.value)} + onChange={(e) => { + setUserAnswer(e.target.value) + if (isWrong) setIsWrong(false) + }} onKeyDown={(e) => { if (e.key === 'Enter') onSubmitAnswer() }} className={`${styles.input} ${isWrong ? styles.wrong : ''}`} aria-label="Your answer" placeholder="?" ref={inputRef} + inputMode="numeric" + pattern="-?[0-9]*" />
285-301: Optional: trap focus inside the end-game dialog.Right now Tab can escape the modal. Consider a small focus trap (e.g., focus-trap-react) or sentinels until it closes. Happy to provide a minimal snippet if you want it dependency-free.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (2)
src/app/game/bullet-math/page.module.css(1 hunks)src/app/game/bullet-math/page.tsx(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/app/game/bullet-math/page.tsx (2)
src/app/game/memory-flip-card/page.tsx (1)
difficulty(47-364)src/app/game/schulte-table/page.tsx (2)
numbers(14-185)startNewGame(45-54)
| .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); | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick (assertive)
Micro-optimization: limit transitioned properties.
Avoid transition: all to reduce layout work; scope to the properties you animate.
.menuButton {
...
- transition: all 0.3s ease;
+ transition: transform 0.2s ease, background 0.2s ease;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| .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 { | |
| 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: transform 0.2s ease, background 0.2s ease; | |
| box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); | |
| } |
🤖 Prompt for AI Agents
In src/app/game/bullet-math/page.module.css around lines 84 to 96, the rule uses
"transition: all 0.3s ease" which is broad and causes extra layout work; replace
it with targeted transitions for only the properties you animate (for example:
background-color, transform, box-shadow, color) and keep the same duration and
easing so the visual effect is preserved while reducing repaint/reflow cost.
| const getPointsForDifficulty = (diff: Difficulty) => { | ||
| switch (diff) { | ||
| case 'amateur': return 1 | ||
| case 'normal': return 2 | ||
| case 'veteran': return 3 | ||
| } | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick (assertive)
Make difficulty-to-points switch exhaustive.
Mirror the exhaustive pattern used in compute so future difficulty additions fail fast.
const getPointsForDifficulty = (diff: Difficulty) => {
switch (diff) {
case 'amateur': return 1
case 'normal': return 2
case 'veteran': return 3
+ default: {
+ const _exhaustive: never = diff
+ return _exhaustive
+ }
}
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const getPointsForDifficulty = (diff: Difficulty) => { | |
| switch (diff) { | |
| case 'amateur': return 1 | |
| case 'normal': return 2 | |
| case 'veteran': return 3 | |
| } | |
| } | |
| const getPointsForDifficulty = (diff: Difficulty) => { | |
| switch (diff) { | |
| case 'amateur': return 1 | |
| case 'normal': return 2 | |
| case 'veteran': return 3 | |
| default: { | |
| const _exhaustive: never = diff | |
| return _exhaustive | |
| } | |
| } | |
| } |
🤖 Prompt for AI Agents
In src/app/game/bullet-math/page.tsx around lines 185 to 191, the
getPointsForDifficulty switch is non‑exhaustive; update it to mirror the
exhaustive pattern used in compute by handling all Difficulty possibilities
explicitly and adding a default (or a never/assertUnreachable) branch that
throws an error so that adding a new Difficulty will fail at compile/run time;
ensure the function still returns a number and use the same assertUnreachable
helper or throw new Error('Unhandled difficulty: ' + diff) in the default
branch.
| <div className={styles.statusBar} aria-live="polite"> | ||
| <span>⏳ {timeLeft}s</span> | ||
| <span>⭐ {score}</span> | ||
| <span>📊 {attempted} attempted</span> | ||
| </div> |
There was a problem hiding this comment.
🧹 Nitpick (assertive)
Announce updates more reliably.
Add role and atomic to ensure concise announcements of time/score/attempts.
- <div className={styles.statusBar} aria-live="polite">
+ <div className={styles.statusBar} role="status" aria-live="polite" aria-atomic="true">📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <div className={styles.statusBar} aria-live="polite"> | |
| <span>⏳ {timeLeft}s</span> | |
| <span>⭐ {score}</span> | |
| <span>📊 {attempted} attempted</span> | |
| </div> | |
| <div className={styles.statusBar} role="status" aria-live="polite" aria-atomic="true"> | |
| <span>⏳ {timeLeft}s</span> | |
| <span>⭐ {score}</span> | |
| <span>📊 {attempted} attempted</span> | |
| </div> |
🤖 Prompt for AI Agents
In src/app/game/bullet-math/page.tsx around lines 247 to 251, the status bar
uses aria-live="polite" but lacks explicit role and atomic settings; update the
div to include role="status" and aria-atomic="true" (keeping aria-live="polite")
so screen readers announce the time/score/attempts as a single, concise update.
There was a problem hiding this comment.
Actionable comments posted: 4
♻️ Duplicate comments (11)
src/app/game/bullet-math/page.tsx (5)
130-134: Timer typing/cleanup: remove unsafe casts and always null the ref.Use window.setInterval/clearInterval and a numeric ref to avoid brittle casts and double-clears. This also aligns with client-only execution.
- const timerRef = useRef<ReturnType<typeof setInterval> | null>(null) + const timerRef = useRef<number | null>(null) @@ - timerRef.current = setInterval(() => { + timerRef.current = window.setInterval(() => { setTimeLeft(t => { if (t <= 1) { - clearInterval(timerRef.current as unknown as number) + if (timerRef.current != null) { + window.clearInterval(timerRef.current) + timerRef.current = null + } setRunning(false) setGameOver(true) return 0 } return t - 1 }) }, 1000) return () => { - if (timerRef.current) clearInterval(timerRef.current as unknown as number) + if (timerRef.current != null) { + window.clearInterval(timerRef.current) + timerRef.current = null + } }Also applies to: 147-163
186-192: Make difficulty-to-points switch exhaustive.Small safety win like you did for Op.
const getPointsForDifficulty = (diff: Difficulty) => { switch (diff) { case 'amateur': return 1 case 'normal': return 2 case 'veteran': return 3 + default: { + const _exhaustive: never = diff + return _exhaustive + } } }
235-246: Group radios and add radiogroup semantics.Improves screen reader announcements and keyboard UX.
- <div className={styles.controls}> - <div className={styles.radioGroup}> + <div className={styles.controls}> + <div className={styles.radioGroup} role="radiogroup" aria-label="Difficulty"> <label> - <input type="radio" checked={difficulty === 'amateur'} onChange={() => setDifficulty('amateur')} disabled={running}/> Amateur + <input name="difficulty" type="radio" checked={difficulty === 'amateur'} onChange={() => setDifficulty('amateur')} disabled={running}/> Amateur </label> <label> - <input type="radio" checked={difficulty === 'normal'} onChange={() => setDifficulty('normal')} disabled={running}/> Normal + <input name="difficulty" type="radio" checked={difficulty === 'normal'} onChange={() => setDifficulty('normal')} disabled={running}/> Normal </label> <label> - <input type="radio" checked={difficulty === 'veteran'} onChange={() => setDifficulty('veteran')} disabled={running}/> Veteran + <input name="difficulty" type="radio" checked={difficulty === 'veteran'} onChange={() => setDifficulty('veteran')} disabled={running}/> Veteran </label> </div>
249-253: Announce status updates more reliably.Add role and atomic so SRs read concise changes.
- <div className={styles.statusBar} aria-live="polite"> + <div className={styles.statusBar} role="status" aria-live="polite" aria-atomic="true">
259-268: Clear error state while typing and hint mobile keyboards.Reset the red state on edit and help phones show a numeric keypad.
<input type="text" value={userAnswer} - onChange={(e) => setUserAnswer(e.target.value)} + onChange={(e) => { + setUserAnswer(e.target.value) + if (isWrong) setIsWrong(false) + }} onKeyDown={(e) => { if (e.key === 'Enter') onSubmitAnswer() }} className={`${styles.input} ${isWrong ? styles.wrong : ''}`} aria-label="Your answer" placeholder="?" ref={inputRef} + inputMode="numeric" />src/app/game/bullet-math/page.module.css (6)
23-34: Prevent overflow on very narrow screens.Make the minimum responsive.
- min-width: 360px; + min-width: min(360px, 100%);
36-40: Remove the commented-out .buttons block.Dead code adds noise; safe to delete.
-/* .buttons { - display: flex; - gap: 16px; - margin-top: 10px; -} */
51-54: Drop unused .true/.false variants.They aren’t referenced; trim CSS surface.
-.true { background: #16a34a; color: white; } -.false { background: #dc2626; color: white; } .neutral { background: #334155; color: white; }
84-96: Limit transitioned properties (avoid transition: all).Reduces layout/repaint work.
- transition: all 0.3s ease; + transition: transform 0.2s ease, background 0.2s ease, box-shadow 0.2s ease;
103-121: Respect prefers-reduced-motion.Disable hover transforms for motion-sensitive users.
.menuButton:hover { background: linear-gradient(135deg, #2563eb, #1e40af); transform: translateY(-2px); } + +@media (prefers-reduced-motion: reduce) { + .menuButton, + .resetButton { + transition: none; + } + .menuButton:hover, + .resetButton:hover { + transform: none; + } +}
143-159: Mobile tweaks look good; keep the responsive min-width consistent.Mirror the responsive min-width change here too.
.equationBox { - min-width: auto; + min-width: auto; /* already responsive above via min(360px, 100%) */
📜 Review details
Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (2)
src/app/game/bullet-math/page.module.css(1 hunks)src/app/game/bullet-math/page.tsx(1 hunks)
🔇 Additional comments (2)
src/app/game/bullet-math/page.tsx (2)
25-36: Nice exhaustive switch for Op.This is clean and future-safe. Great use of the never check.
197-206: Good fix: attempts increment only after valid numeric input.This now matches “attempted questions” semantics and avoids counting empty/invalid submits. Nice.
| 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') { |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Match PR spec: Amateur should use single-digit operands for +/-.
Your PR description says “single-digit operations” for Amateur, but +/− currently use 1–20. Suggest 1–9.
- } else {
- a = randint(1, 20); b = randint(1, 20)
+ } else {
+ a = randint(1, 9); b = randint(1, 9)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| 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 (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, 9); b = randint(1, 9) | |
| } | |
| } else if (diff === 'normal') { |
🤖 Prompt for AI Agents
In src/app/game/bullet-math/page.tsx around lines 52 to 61, the amateur
difficulty currently generates operands for + and - using randint(1, 20); change
that so amateur uses single-digit operands like the PR spec: replace randint(1,
20) with randint(1, 9) in the else branch (leave the ÷ and × branches as they
are).
| export default function BulletMath() { | ||
| type Difficulty = 'amateur' | 'normal' | 'veteran' | ||
| const [difficulty, setDifficulty] = useState<Difficulty | undefined>(undefined) | ||
| const [running, setRunning] = useState(false) |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Blocker: difficulty can be undefined and '' is assigned later — breaks types and Start flow.
Today Start can run with no difficulty, and reset sets an invalid value (''). Let’s make difficulty always valid and remove the duplicate type inside the component.
Apply:
-export default function BulletMath() {
- type Difficulty = 'amateur' | 'normal' | 'veteran'
- const [difficulty, setDifficulty] = useState<Difficulty | undefined>(undefined)
+export default function BulletMath() {
+ // Difficulty is already declared at module scope; no need to redeclare inside.
+ const [difficulty, setDifficulty] = useState<Difficulty>('amateur') const startGame = useCallback(() => {
@@
- setEquation(generateEquation(difficulty))
+ setEquation(generateEquation(difficulty)) const getPointsForDifficulty = (diff: Difficulty) => { const resetGame = () => {
@@
- // reset difficulty selection so user can pick again
- setDifficulty('')
+ // keep prior selection; radios are re-enabled when not runningAlso applies to: 136-145, 186-192, 209-217, 219-229
| <h2>🔬 Scientific Backing</h2> | ||
| <p> | ||
| Research shows that regular mental arithmetic can improve working memory and | ||
| cognitive flexibility. See: <i>“Cognitive Benefits of Mental Calculation” – | ||
| Smith et al., 2020</i>. | ||
| </p> |
There was a problem hiding this comment.
🧹 Nitpick (assertive)
Avoid unverifiable citation in UI.
“Smith et al., 2020” isn’t a linked/known reference here. Either link to a real source or rephrase generically.
- Research shows that regular mental arithmetic can improve working memory and
- cognitive flexibility. See: <i>“Cognitive Benefits of Mental Calculation” –
- Smith et al., 2020</i>.
+ Research suggests regular mental arithmetic may support working memory and
+ cognitive flexibility.📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <h2>🔬 Scientific Backing</h2> | |
| <p> | |
| Research shows that regular mental arithmetic can improve working memory and | |
| cognitive flexibility. See: <i>“Cognitive Benefits of Mental Calculation” – | |
| Smith et al., 2020</i>. | |
| </p> | |
| <h2>🔬 Scientific Backing</h2> | |
| <p> | |
| Research suggests regular mental arithmetic may support working memory and | |
| cognitive flexibility. | |
| </p> |
🤖 Prompt for AI Agents
In src/app/game/bullet-math/page.tsx around lines 329 to 334, the UI includes an
unverifiable citation “Smith et al., 2020”; either replace it with a real,
linked source (add a proper URL and update the text to include the full citation
and ensure link opens in a new tab) or rephrase the sentence to a generic claim
without a specific author (e.g., “Research shows” or “Studies suggest”) and
remove the “Smith et al., 2020” fragment so the UI no longer presents an
unreferenced citation.
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (8)
src/app/game/bullet-math/page.tsx (8)
52-60: Align Amateur spec to single‑digit +/−.PR says “single-digit operations” for Amateur; +/− currently use 1–20.
Apply:
- } else { - a = randint(1, 20); b = randint(1, 20) + } else { + a = randint(1, 9); b = randint(1, 9)
118-121: Blocker: difficulty can be undefined; also redeclared type.generateEquation expects Difficulty, but state is Difficulty | undefined and you redeclare the type locally.
Apply:
-export default function BulletMath() { - type Difficulty = 'amateur' | 'normal' | 'veteran' - const [difficulty, setDifficulty] = useState<Difficulty | undefined>(undefined) +export default function BulletMath() { + // Use the module-level Difficulty and default to a valid selection + const [difficulty, setDifficulty] = useState<Difficulty>('amateur')Also applies to: 141-145
130-134: Harden timer: remove unsafe casts, use window. and null the ref.*Prevents type hacks, double-clears, and leaks.
Apply:
- const timerRef = useRef<ReturnType<typeof setInterval> | null>(null) + const timerRef = useRef<number | null>(null) @@ - timerRef.current = setInterval(() => { + timerRef.current = window.setInterval(() => { setTimeLeft(t => { if (t <= 1) { - clearInterval(timerRef.current as unknown as number) + if (timerRef.current != null) { + window.clearInterval(timerRef.current) + timerRef.current = null + } setRunning(false) setGameOver(true) return 0 } return t - 1 }) }, 1000) return () => { - if (timerRef.current) clearInterval(timerRef.current as unknown as number) + if (timerRef.current != null) { + window.clearInterval(timerRef.current) + timerRef.current = null + } }Also applies to: 147-163
186-192: Make points switch exhaustive.Future Difficulty additions will then fail fast.
Apply:
const getPointsForDifficulty = (diff: Difficulty) => { switch (diff) { case 'amateur': return 1 case 'normal': return 2 case 'veteran': return 3 + default: { + const _exhaustive: never = diff + return _exhaustive + } } }
235-246: Group radios and add radiogroup semantics.Improves screen reader behavior and keyboard UX.
Apply:
- <div className={styles.radioGroup}> + <div className={styles.radioGroup} role="radiogroup" aria-label="Difficulty"> <label> - <input type="radio" checked={difficulty === 'amateur'} onChange={() => setDifficulty('amateur')} disabled={running}/> Amateur + <input name="difficulty" type="radio" checked={difficulty === 'amateur'} onChange={() => setDifficulty('amateur')} disabled={running}/> Amateur </label> <label> - <input type="radio" checked={difficulty === 'normal'} onChange={() => setDifficulty('normal')} disabled={running}/> Normal + <input name="difficulty" type="radio" checked={difficulty === 'normal'} onChange={() => setDifficulty('normal')} disabled={running}/> Normal </label> <label> - <input type="radio" checked={difficulty === 'veteran'} onChange={() => setDifficulty('veteran')} disabled={running}/> Veteran + <input name="difficulty" type="radio" checked={difficulty === 'veteran'} onChange={() => setDifficulty('veteran')} disabled={running}/> Veteran </label> </div>
249-253: Polish live region announcements.Add role and atomic so updates are read succinctly.
Apply:
- <div className={styles.statusBar} aria-live="polite"> + <div className={styles.statusBar} role="status" aria-live="polite" aria-atomic="true">
260-268: Clear error on edit and hint mobile keypads.Resets red state immediately and improves mobile input UX.
Apply:
<input type="text" value={userAnswer} - onChange={(e) => setUserAnswer(e.target.value)} + onChange={(e) => { + setUserAnswer(e.target.value) + if (isWrong) setIsWrong(false) + }} onKeyDown={(e) => { if (e.key === 'Enter') onSubmitAnswer() }} className={`${styles.input} ${isWrong ? styles.wrong : ''}`} aria-label="Your answer" placeholder="?" ref={inputRef} + inputMode="numeric" + pattern="-?[0-9]*" />
329-334: Avoid unverifiable citation in UI.Rephrase or link a real source.
Apply:
- Research shows that regular mental arithmetic can improve working memory and - cognitive flexibility. See: <i>“Cognitive Benefits of Mental Calculation” – - Smith et al., 2020</i>. + Research suggests regular mental arithmetic may support working memory and + cognitive flexibility.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (1)
src/app/game/bullet-math/page.tsx(1 hunks)
🔇 Additional comments (2)
src/app/game/bullet-math/page.tsx (2)
25-36: Nice: exhaustive op switch.Good defensive typing with the never check. Solid.
197-206: Good fix: attempts increment after input validation.This matches the PR note and avoids counting empty/non‑numeric submits. 👍
| // 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 } | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick (assertive)
Edge‑case guard in makeDivisible.
If ceil(aMin/b) > floor(aMax/b), randint(min,max) can underflow. Not hit by current ranges, but easy to harden.
Apply:
- const k = randint(Math.max(1, Math.ceil(aMin / b)), Math.max(1, Math.floor(aMax / b)))
+ const kMin = Math.max(1, Math.ceil(aMin / b))
+ const kMax = Math.max(kMin, Math.floor(aMax / b))
+ const k = randint(kMin, kMax)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // 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 } | |
| } | |
| // 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 kMin = Math.max(1, Math.ceil(aMin / b)) | |
| const kMax = Math.max(kMin, Math.floor(aMax / b)) | |
| const k = randint(kMin, kMax) | |
| const a = b * k | |
| return { a, b } | |
| } |
🤖 Prompt for AI Agents
In src/app/game/bullet-math/page.tsx around lines 38 to 45, the computation of k
uses randint(Math.max(1, Math.ceil(aMin / b)), Math.max(1, Math.floor(aMax /
b))) which can underflow if Math.ceil(aMin/b) > Math.floor(aMax/b); compute minK
= Math.max(1, Math.ceil(aMin / b)) and maxK = Math.floor(aMax / b), then if maxK
< minK set maxK = minK (or clamp maxK to minK) before calling randint(minK,
maxK) so the range is always valid; keep the existing b zero-guard.
| {gameOver && ( | ||
| <div className={styles.overlay} role="dialog" aria-modal="true" aria-labelledby="times-up-heading"> | ||
| <div className={styles.completionMessage}> | ||
| <h2 id="times-up-heading">🎉 Time’s Up!</h2> | ||
| <div className={styles.finalStats}> | ||
| <p>⭐ Score: {score}</p> | ||
| <p>📊 Questions Attempted: {attempted}</p> | ||
| </div> | ||
| <button type="button" className={styles.resetButton} onClick={resetGame} ref={resetRef}> | ||
| 🔄 Play Again | ||
| </button> | ||
| <button type="button" className={styles.menuButton} onClick={() => router.push('/')}> | ||
| 🏠 Back to Homepage | ||
| </button> | ||
| </div> | ||
| </div> | ||
| )} |
There was a problem hiding this comment.
🧹 Nitpick (assertive)
Optional: trap focus inside the end‑game dialog.
Currently Tab can escape the modal; add a lightweight focus trap (library or sentinels) while gameOver is true.
Want a minimal, dependency‑free trap snippet?

🧮 New Game: Bullet Math
This PR adds a new brain game Bullet Math under
src/app/game/bullet-math/.✨ Features
🛠️ Fixes/Changes
src/app/game/page.tsxto include Bullet Math entry in the game selection.src/app/game/bullet-math/containing:page.tsx→ Game logicpage.module.css→ Stylingany, usedReturnType<typeof setInterval>).aria-labelto inputtype="button"on buttonsrole="dialog"witharia-labelledbyautoFocusto input and reset button:focus-visibleoutline styles✅ Checklist
🙏 Acknowledgements
no-explicit-anyESLint error)..input,.menuButton)..startBtnusage.autoFocusand focus-ring styles for accessibility.This PR introduces a fresh math challenge game to the project 🚀