Visitar URL original
Feature/game framework improvements by cwchanap · Pull Request #17 · cwchanap/cetus · GitHub
Skip to content

Feature/game framework improvements - #17

Merged
cwchanap merged 14 commits into
mainfrom
feature/game-framework-improvements
Feb 8, 2026
Merged

cwchanap merged 14 commits into
mainfrom
feature/game-framework-improvements

Conversation

@cwchanap

@cwchanap cwchanap commented Feb 2, 2026 •

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • New Features

    • Structured game error API for consistent error handling
    • Expanded user/profile schema and analytics (display names, usernames, stats, scores, achievements)
    • New Snake game (game, renderer, init flow) and shared game utilities (random, math, geometry, formatting, easing, debounce/throttle)
    • New helper to update user level; challenge flow updated to use it
  • Bug Fixes

    • Improved teardown/cleanup across games: stops loops, removes listeners, destroys renderers
    • Safer init flows and lifecycle guards
  • Tests

    • Large expansion of unit tests across games and core utilities
  • Documentation

    • Removed internal contributor instructions file
  • Chores

    • DB client initialization and config adjustments; minor CI/config comments and allow-list tweak

- Add centralized error handling in games/core/errors.ts
- Add shared utilities for game validation and logic
- Add unit tests for multiple games (evader, path-navigator, sudoku, snake)
- Improve snake game with framework implementation and proper renderer
- Add database schema consolidation migration
- Update game initialization files to use new error handling
- Add proper event handler cleanup with named functions in Tetris and Bubble Shooter
- Fix memory leaks by canceling animation frames and removing event listeners
- Enhance create2DArray utility to support factory functions for unique objects
- Fix Snake game grid positioning to center properly based on grid dimensions
- Add explicit foreign_keys PRAGMA to database client and migration
- Improve test assertions and reliability across multiple game tests
- Add UNKNOWN_ERROR code to game error types
Copilot AI review requested due to automatic review settings February 2, 2026 05:33
@vercel

vercel Bot commented Feb 2, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cetus Ready Ready Preview, Comment Feb 8, 2026 2:41am

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@coderabbitai

coderabbitai Bot commented Feb 2, 2026 •

Copy link
Copy Markdown

Warning

Rate limit exceeded

@cwchanap has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 7 minutes and 38 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📝 Walkthrough

Walkthrough

Adds a structured game error system, shared utilities, a full Snake feature (logic, renderer, framework), many new/expanded game tests, safer init/cleanup patterns for multiple games, DB schema and client changes plus an updateUserLevel query, challengeService updates, and deletes the Copilot instructions doc.

Changes

Cohort / File(s) Summary
Documentation
\.github/copilot-instructions.md
Deleted Copilot instructions/documentation file.
DB schema & client
better-auth_migrations/2025-07-06-schema-consolidation.sql, src/lib/server/db/client.ts
Adds user profiling tables/columns (user_stats, game_scores, user_achievements, displayName, username), unique/indexes; switches LibSQL init to client-based pattern and enables PRAGMA foreign_keys.
DB queries & services
src/lib/server/db/queries.ts, src/lib/services/challengeService.ts, src/lib/services/challengeService.test.ts
Adds updateUserLevel(userId,newLevel), replaces previous XP-update usage with level-only updates in challenge flow, and updates tests/mocks accordingly.
Core error system
src/lib/games/core/errors.ts, src/lib/games/core/errors.test.ts, src/lib/games/core/index.ts
New GameError base class, specialized errors, codes, guards, wrap/handle utilities, unit tests, and re-exports from core index.
Shared utilities
src/lib/games/shared/utils.ts, src/lib/games/shared/utils.test.ts
Adds broad game utilities (random, math/geometry, formatting, debounce/throttle, id/2D array/deepClone, easing) and comprehensive tests.
Snake feature
src/lib/games/snake/SnakeGame.ts, src/lib/games/snake/SnakeRenderer.ts, src/lib/games/snake/types.ts, src/lib/games/snake/initFramework.ts, src/lib/games/snake/SnakeGame.test.ts, src/pages/snake/index.astro
Full Snake implementation: types, game logic, PixiJS renderer, framework initializer (returns cleanup/restart/getState/endGame), extensive tests, and page updated to use the new initializer.
Game init / cleanup patterns
src/lib/games/bubble-shooter/init.ts, src/lib/games/tetris/init.ts
Adds idempotent cleanup() methods, draw loop guards (drawFrameId/drawRunning), AbortController/centralized handlers, and safer renderer teardown.
Additional game tests
src/lib/games/evader/game.test.ts, src/lib/games/path-navigator/game.test.ts, src/lib/games/sudoku/game.test.ts, src/lib/games/snake/SnakeGame.test.ts
Adds/expands unit tests covering lifecycle, timers, spawning, movement, levels, scoring, state access, and cleanup across multiple games.
Score manager logging
src/lib/games/core/ScoreManager.ts
Minor logging change in final score save catch: logs error via console.error.
Config & tooling
.claude/settings.local.json, playwright.config.ts
Allow bunx tsc:* in Claude local settings; add CI-related comment in Playwright config.
Server DB helper
src/lib/server/db/queries.ts
New helper updateUserLevel added (wrapper around upsert behavior) with error handling.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Page as Snake Page
    participant Framework as initSnakeGameFramework
    participant Game as SnakeGame
    participant Renderer as SnakeRenderer
    participant DOM

    User->>Page: load
    Page->>Framework: initSnakeGameFramework(config, callbacks)
    Framework->>DOM: validate container
    Framework->>Renderer: instantiate renderer
    Renderer->>DOM: create Pixi app / draw initial state
    Framework->>Game: instantiate SnakeGame
    Framework->>DOM: wire controls & keyboard
    Framework->>Game: game.start()
    Game->>Game: run game loop (rAF)
    User->>DOM: input (arrow keys / controls)
    DOM->>Framework: forward input
    Framework->>Game: changeDirection / pause / resume
    Game->>Framework: emit state change
    Framework->>Renderer: renderGame(state)
    Renderer->>DOM: update visuals
    Game->>Framework: emit end event
    Framework->>DOM: show overlay / call achievements hook
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰 I hopped where snakes and scores take flight,

Errors wrapped snug in the soft debug night,
Utilities ready, tests lined like carrots,
DB seeds sown, levels climb with merits,
Cleanup called — I nibble code till light.

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 76.32% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Feature/game framework improvements' is vague and uses generic phrasing that doesn't convey what specific improvements were made to the game framework. Replace with a more specific title that describes the main improvement, such as 'Add error handling system and game lifecycle cleanup' or 'Implement Snake game with base framework patterns'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/game-framework-improvements

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov

codecov Bot commented Feb 2, 2026 •

Copy link
Copy Markdown

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request implements significant improvements to the game framework infrastructure with a focus on resource management, error handling, and code organization.

Changes:

  • Migrates Snake game to new BaseGame framework with improved architecture
  • Adds cleanup/disposal methods to Tetris and Bubble Shooter games for proper resource management
  • Introduces comprehensive error handling framework with typed errors and recovery logic
  • Adds shared utility functions library for common game operations
  • Expands test coverage with unit tests for Snake, Sudoku, Path Navigator, Evader, shared utils, and error handling
  • Updates database client initialization with foreign key enforcement
  • Adds database schema consolidation migration
  • Removes .github/copilot-instructions.md documentation file

Reviewed changes

Copilot reviewed 19 out of 19 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/pages/snake/index.astro Simplified Snake game page using new framework initialization
src/lib/server/db/client.ts Added foreign keys pragma execution and updated client initialization
src/lib/games/tetris/init.ts Added cleanup method for proper resource disposal
src/lib/games/sudoku/game.test.ts Comprehensive unit tests for Sudoku game logic
src/lib/games/snake/types.ts Extended types for BaseGame framework integration
src/lib/games/snake/initFramework.ts New framework-based initialization replacing legacy init
src/lib/games/snake/SnakeRenderer.ts PixiJS renderer implementation using BaseRenderer
src/lib/games/snake/SnakeGame.ts Core game logic using BaseGame framework
src/lib/games/snake/SnakeGame.test.ts Comprehensive unit tests for Snake game
src/lib/games/shared/utils.ts Shared utility functions (random, array, formatting, etc.)
src/lib/games/shared/utils.test.ts Unit tests for shared utilities
src/lib/games/path-navigator/game.test.ts Unit tests for Path Navigator game
src/lib/games/evader/game.test.ts Unit tests for Evader game
src/lib/games/core/index.ts Added error handling exports
src/lib/games/core/errors.ts New error handling framework with typed errors
src/lib/games/core/errors.test.ts Unit tests for error handling
src/lib/games/bubble-shooter/init.ts Added cleanup method with idempotency guard
better-auth_migrations/2025-07-06-schema-consolidation.sql Database schema consolidation migration
.github/copilot-instructions.md Removed documentation file

Comment thread src/lib/server/db/client.ts Outdated
Comment thread src/lib/games/shared/utils.ts Outdated
Comment thread better-auth_migrations/2025-07-06-schema-consolidation.sql
Comment thread src/lib/games/tetris/init.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/lib/games/tetris/init.ts (1)

23-27: ⚠️ Potential issue | 🟡 Minor

Interface missing cleanup method declaration.

The TetrisGameInstance interface doesn't include the cleanup method that's actually returned by initTetrisGame. This causes a type mismatch.

🔧 Proposed fix
 export interface TetrisGameInstance {
     restart: () => void
     getState: () => GameState
     endGame: () => Promise<void>
+    cleanup: () => void
 }
🤖 Fix all issues with AI agents
In `@better-auth_migrations/2025-07-06-schema-consolidation.sql`:
- Around line 10-13: The migration file currently leaves ALTER TABLE statements
commented and relies on a lazy-run function ensureUserIdentityColumns() in
src/lib/server/db/queries.ts that creates columns per-query and only logs
failures; move ensureUserIdentityColumns() out of per-query paths and call it
once at application startup (for example from src/middleware.ts or an app init
hook) so columns are created deterministically and any errors can be surfaced
and handled, or alternatively uncomment the ALTER TABLE statements in the SQL
migration and keep a safe guard (e.g., conditional CREATE/ALTER IF NOT EXISTS)
so the migration file accurately performs the schema change rather than relying
on runtime lazy creation.

In `@src/lib/games/shared/utils.ts`:
- Around line 163-168: The generateId function uses the deprecated
String.prototype.substr; update generateId to use String.prototype.slice instead
by replacing the substr(2, 9) call on the random string with an equivalent
slice(2, 2 + 9) (or slice(2, 11)) expression so the produced ID remains the same
format; adjust only the expression inside generateId to remove the deprecated
API.

In `@src/lib/games/tetris/init.ts`:
- Around line 236-265: The cleanup function can run multiple times and may call
renderer.app.destroy or remove listeners repeatedly; add an idempotency guard
(e.g., a boolean cleanupCalled) near the top of the cleanup closure to return
immediately if already true, set cleanupCalled = true before performing
teardown, and use the existing symbols (cleanup closure, cleanupCalled flag
name, renderer.app.destroy(...), and handlers
startHandler/pauseHandler/resetHandler/restartHandler/beforeUnloadHandler/keydownHandler)
so repeated invocations are safe and destructor/detach logic only runs once.

In `@src/lib/server/db/client.ts`:
- Around line 14-20: The startup code directly calls dbClient.execute('PRAGMA
foreign_keys = ON') without using Kysely's sql template or handling async
failures; replace that line with a Kysely-executed PRAGMA using the sql tag
(e.g., await sql`PRAGMA foreign_keys = ON`) and wrap it in a try/catch so
startup DB errors are logged/handled (use your existing logger or throw/exit on
failure). Locate dbClient, the exported dialect/LibsqlDialect and the PRAGMA
call, import/use the Kysely sql template, await the statement, and add explicit
error handling around it to fail fast with a clear log if the PRAGMA or
connection initialization fails.
🧹 Nitpick comments (14)
src/lib/games/snake/SnakeGame.ts (2)

180-182: Consider defensive handling for empty snake array.

getHead() accesses this.state.snake[0] directly. While the snake should never be empty during normal gameplay (initialized with 3 segments), accessing index 0 on an empty array returns undefined, which doesn't match the Position return type.

🛡️ Optional defensive check
     getHead(): Position {
+        if (this.state.snake.length === 0) {
+            return { x: 0, y: 0 }
+        }
         return this.state.snake[0]
     }

246-259: Type inconsistency when calling utility functions.

The generateFoodPosition and isOutOfBounds functions expect a GameConstants interface, but you're passing partial objects with only GRID_WIDTH and GRID_HEIGHT. This works at runtime but creates a type mismatch.

Consider creating a minimal interface or updating the utility functions to accept only the required fields.

src/lib/games/snake/SnakeGame.test.ts (1)

165-181: Consider adding tests for movement and collision scenarios.

The current test suite covers initialization, lifecycle, and callbacks well. However, tests for actual gameplay mechanics would strengthen coverage:

  • Snake movement and food consumption
  • Wall collision detection
  • Self-collision detection
  • Game over on timer expiration

These are important for ensuring game state transitions work correctly during gameplay. Based on learnings, tests should ensure proper game state transitions and score submissions.

Would you like me to generate additional test cases for movement and collision scenarios?

src/lib/games/snake/SnakeRenderer.ts (2)

174-182: Address static analysis warning for forEach callback.

The Biome linter flags that the callback should not return a value. While graphic.destroy() returns void (which is fine), the linter prefers explicit handling.

🔧 Proposed fix to satisfy linter
     private clearGameObjects(): void {
         if (this.gameObjectContainer) {
             this.gameObjectContainer.removeChildren()
         }

         // Destroy graphics objects
-        this.graphics.forEach(graphic => graphic.destroy())
+        for (const graphic of this.graphics) {
+            graphic.destroy()
+        }
         this.graphics = []
     }

30-47: Consider adding error handling for setup failures.

If super.setup() fails or getApp() returns null, the method exits silently without indication. Per coding guidelines, async operations should include error handling.

🛡️ Optional: Add logging for setup issues
     async setup(): Promise<void> {
         await super.setup()

         const app = this.getApp()
         if (!app) {
+            console.warn('SnakeRenderer: Failed to get PIXI app after setup')
             return
         }
src/lib/games/snake/initFramework.ts (2)

252-274: Consolidate duplicate handler functions.

resetHandler, restartHandler, and playAgainHandler have identical implementations. Consider consolidating into a single handler.

♻️ Proposed consolidation
-    const resetHandler = () => {
-        game.reset()
-        const overlay = document.getElementById('game-over-overlay')
-        if (overlay) {
-            overlay.classList.add('hidden')
-        }
-    }
-
-    const restartHandler = () => {
-        game.reset()
-        const overlay = document.getElementById('game-over-overlay')
-        if (overlay) {
-            overlay.classList.add('hidden')
-        }
-    }
-
-    const playAgainHandler = () => {
+    const resetHandler = () => {
         game.reset()
         const overlay = document.getElementById('game-over-overlay')
         if (overlay) {
             overlay.classList.add('hidden')
         }
     }
+
+    // Use same handler for all reset-like actions
+    const restartHandler = resetHandler
+    const playAgainHandler = resetHandler

132-142: Render loop runs continuously regardless of game state.

The render loop calls renderer.render(game.getState()) on every animation frame, even when the game is paused or not started. While the renderer's needsRedraw flag provides some optimization, consider adding a state check to reduce unnecessary work.

⚡ Optional optimization
     function renderLoop() {
         if (!renderRunning) {
             return
         }
-        renderer.render(game.getState())
+        const state = game.getState()
+        if (state.isActive || state.needsRedraw) {
+            renderer.render(state)
+        }
         renderFrameId = requestAnimationFrame(renderLoop)
     }
src/lib/games/bubble-shooter/init.ts (1)

150-157: Consider adding touch event support for mobile compatibility.

The canvas only registers mouse events. Per coding guidelines, games should implement proper touch and mouse event handling for mobile/desktop compatibility.

♻️ Proposed touch event additions
         // Canvas event listeners
         if (renderer.app && renderer.app.canvas) {
             renderer.app.canvas.addEventListener(
                 'mousemove',
                 canvasMouseMoveHandler
             )
             renderer.app.canvas.addEventListener('click', canvasClickHandler)
+            renderer.app.canvas.addEventListener(
+                'touchmove',
+                (e: TouchEvent) => {
+                    e.preventDefault()
+                    const touch = e.touches[0]
+                    const mouseEvent = new MouseEvent('mousemove', {
+                        clientX: touch.clientX,
+                        clientY: touch.clientY,
+                    })
+                    canvasMouseMoveHandler(mouseEvent)
+                }
+            )
+            renderer.app.canvas.addEventListener(
+                'touchend',
+                (e: TouchEvent) => {
+                    e.preventDefault()
+                    const touch = e.changedTouches[0]
+                    const mouseEvent = new MouseEvent('click', {
+                        clientX: touch.clientX,
+                        clientY: touch.clientY,
+                    })
+                    canvasClickHandler(mouseEvent)
+                }
+            )
         }

As per coding guidelines: "Implement proper touch and mouse event handling for mobile/desktop compatibility".

src/lib/games/shared/utils.ts (1)

187-192: Document deepClone limitations.

The JSON-based clone doesn't handle functions, undefined, Date, RegExp, circular references, or Symbol keys. Consider expanding the JSDoc to warn users.

📝 Proposed documentation enhancement
 /**
  * Deep clone an object (simple version using JSON)
+ * 
+ * Limitations: Does not handle functions, undefined, Date objects,
+ * RegExp, circular references, or Symbol keys.
  */
 export function deepClone<T>(obj: T): T {
     return JSON.parse(JSON.stringify(obj))
 }
src/lib/games/shared/utils.test.ts (1)

199-218: Consider using consistent fake timer setup pattern.

The throttle test manages fake timers inline while debounce uses beforeEach/afterEach. For consistency and to prevent timer leaks in case of test failures, consider using the same pattern.

♻️ Proposed refactor for consistency
 describe('throttle', () => {
+    beforeEach(() => {
+        vi.useFakeTimers()
+    })
+
+    afterEach(() => {
+        vi.useRealTimers()
+    })
+
     it('should throttle function calls', () => {
-        vi.useFakeTimers()
         const fn = vi.fn()
         const throttled = throttle(fn, 100)

         throttled()
         expect(fn).toHaveBeenCalledTimes(1)

         throttled()
         throttled()
         expect(fn).toHaveBeenCalledTimes(1)

         vi.advanceTimersByTime(100)
         throttled()
         expect(fn).toHaveBeenCalledTimes(2)
-
-        vi.useRealTimers()
     })
 })
src/lib/games/core/errors.ts (1)

32-46: Consider using GameErrorCodes constant for error codes.

The error code strings are hardcoded in each class and duplicated in the GameErrorCodes constant below. Using the constant would improve maintainability and prevent potential inconsistencies.

♻️ Suggested refactor
+import { GameErrorCodes } from './errors'
+
 export class GameInitializationError extends GameError {
     constructor(
         message: string,
         options?: {
             context?: Record<string, unknown>
             cause?: Error
         }
     ) {
-        super(message, 'GAME_INIT_ERROR', {
+        super(message, GameErrorCodes.GAME_INIT_ERROR, {
             ...options,
             recoverable: true,
         })
         this.name = 'GameInitializationError'
     }
 }

Apply the same pattern to other error classes (RenderError, DOMElementNotFoundError, etc.).

better-auth_migrations/2025-07-06-schema-consolidation.sql (1)

62-65: Consider a composite index for leaderboard queries.

If you frequently query top scores by game (e.g., WHERE game_id = ? ORDER BY score DESC, created_at DESC), a composite index can drastically reduce sort cost.

🔧 Example composite index
+CREATE INDEX IF NOT EXISTS "idx_game_scores_game_score_created"
+  ON "game_scores" ("game_id", "score" DESC, "created_at" DESC);
src/lib/games/evader/game.test.ts (2)

238-252: Add integration coverage for score submission/achievements and responsive canvas behavior.
This suite validates stats, but it doesn’t assert /api/scores submission, achievement thresholds/in‑game event payloads, or responsive/touch canvas behavior. Consider adding a higher‑level test (likely in init/renderer tests) to cover those flows.

Based on learnings: “Ensure proper game state transitions and score submissions in tests”, “Test achievement score thresholds and in-game event achievements”, and “Verify responsive behavior of game interfaces and canvas elements in tests”.


130-162: Make the coin/bomb spawn test deterministic.
The current test only verifies that any object spawns—the .some() check will always pass. To ensure both coin and bomb types can actually spawn, mock Math.random() with specific return values to force each type and verify the expected behavior.

♻️ Proposed refactor (deterministic spawn types)
-        it('should spawn either coins or bombs', () => {
+        it('should deterministically spawn coins and bombs', () => {
+            const randomSpy = vi.spyOn(Math, 'random')
+            randomSpy.mockReturnValueOnce(0.1) // coin
+            randomSpy.mockReturnValueOnce(0.99) // bomb
             game.startGame()
 
-            // Spawn multiple objects
-            vi.advanceTimersByTime(config.spawnInterval * 10 * 1000)
+            vi.advanceTimersByTime(config.spawnInterval * 2 * 1000)
 
             const calls = (
                 mockCallbacks.onObjectSpawn as ReturnType<typeof vi.fn>
             ).mock.calls
             const types = calls.map((call: [GameObject]) => call[0].type)
 
-            expect(types.some(t => t === 'coin' || t === 'bomb')).toBe(true)
+            expect(types).toEqual(['coin', 'bomb'])
+            randomSpy.mockRestore()
         })

Comment thread better-auth_migrations/2025-07-06-schema-consolidation.sql Outdated
Comment thread src/lib/games/shared/utils.ts
Comment thread src/lib/games/tetris/init.ts
Comment thread src/lib/server/db/client.ts Outdated
- Wrap PRAGMA foreign_keys in try/catch with proper error handling
- Fix SnakeRenderer GPU resource leak by properly destroying PIXI containers
- Fix SnakeGame overlapping segments bug by adjusting initial length based on grid width
- Fix CI environment variable handling with explicit comparison ('true'/'1')
- Throw explicit error when SnakeRenderer app is not available after setup
- Properly remove 'end' event listener in Snake cleanup
- Remove state mutation from renderer, move needsRedraw reset to render loop
- Add foundCell tracking to Sudoku test to ensure assertions always run
- Fix error message leading space when context is absent
- Enhance formatTime to handle negative values and display hours (H:MM:SS)
- Add comprehensive documentation to create2DArray for factory behavior
- Fix shuffleArray test to compare sorted copies instead of mutating original
- Fix SnakeGame maxLength test to actually simulate growth by placing food
- Add markRendered() method to SnakeGame to fix state mutation in render loop
- Fix sudoku tests to ensure given/non-given cells are actually found
- Fix beforeUnloadHandler to set event.returnValue for modern browsers
…sely PRAGMA

- Uncomment ALTER TABLE statements in migration with IF NOT EXISTS for index

- Add cleanupCalled guard to tetris cleanup function to prevent double execution

- Use Kysely sql template for PRAGMA foreign_keys with proper error handling

- Add cleanup method to TetrisGameInstance interface
The SCORE_TARGET challenge type requires gameId to match the game being played.

Without gameId in the mock, the challenge was never completed and no XP was earned,

causing the level up test to fail because updateUserLevel was never called.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/lib/games/tetris/init.ts (1)

237-240: ⚠️ Potential issue | 🟡 Minor

restart / endGame remain callable after cleanup destroys the renderer.

Once cleanup() runs, the PixiJS app is destroyed, yet restart() will happily reset state and endGame() may attempt to render or submit scores. Consider either nullifying these methods in cleanup, wrapping them with a guard, or documenting that callers must not invoke them post-cleanup.

🛡️ Lightweight guard approach
     return {
-        restart: () => resetGame(enhancedState, updateNextPieceDisplayFn),
-        getState: () => enhancedState,
-        endGame: () => endGame(enhancedState),
+        restart: () => {
+            if (cleanupCalled) return
+            resetGame(enhancedState, updateNextPieceDisplayFn)
+        },
+        getState: () => enhancedState,
+        endGame: async () => {
+            if (cleanupCalled) return
+            await endGame(enhancedState)
+        },
         cleanup: () => {
🤖 Fix all issues with AI agents
In `@src/lib/games/snake/initFramework.ts`:
- Around line 48-60: Wrap the async call to renderer.initialize() in a try/catch
inside initSnakeGameFramework: call await renderer.initialize() inside a try
block, and in catch perform cleanup of any partially-created renderer resources
(e.g., call a teardown method like renderer.destroy() or renderer.dispose() if
available, or remove the canvas from DOM via renderer.getApp().canvas) then log
the error via the existing logger and rethrow or return a rejected promise;
ensure subsequent code that styles the canvas (renderer.getApp()) only runs
after successful initialization.

In `@src/lib/games/snake/SnakeGame.ts`:
- Around line 209-229: startGameLoop creates a per-instance
requestAnimationFrame loop that calls gameUpdate which triggers emitStateChange
→ onStateChange → renderer.render, while the framework-level rAF loop
(initFramework) also calls renderer.render and markRendered; this double rAF
causes each update to render twice because needsRedraw isn’t cleared until
markRendered. Fix by choosing one render driver: either remove renderer.render
from the SnakeGame onStateChange path (so startGameLoop only updates state and
relies on the framework rAF to render and call markRendered), or stop the
framework rAF for game instances and instead, after gameUpdate/emitStateChange
in startGameLoop, call renderer.render(...) and then markRendered() to clear
needsRedraw; modify the code near
startGameLoop/gameUpdate/emitStateChange/onStateChange and the framework render
loop accordingly to ensure only one rAF path performs renderer.render and
markRendered.

In `@src/lib/games/snake/SnakeRenderer.ts`:
- Around line 184-199: The cleanup() method currently calls
this.gridGraphic.destroy() and then this.gridContainer.destroy({ children: true
}), risking double-destroy of gridGraphic; update cleanup() to choose one
approach: either remove the explicit this.gridGraphic.destroy() and let
this.gridContainer.destroy({ children: true }) recursively destroy its children,
or keep explicit this.gridGraphic.destroy() and call
this.gridContainer.destroy({ children: false }) (or without children) instead;
adjust the code paths around cleanup(), gridContainer and gridGraphic (and
related this.gameObjectContainer handling) so only one destroy is performed for
gridGraphic.
🧹 Nitpick comments (10)
src/lib/games/tetris/init.ts (2)

139-142: resetHandler and restartHandler are identical.

Both close over the same arguments and call resetGame(enhancedState, updateNextPieceDisplayFn). You could share a single handler reference to reduce duplication.

♻️ Suggested consolidation
-    const resetHandler = () =>
-        resetGame(enhancedState, updateNextPieceDisplayFn)
-    const restartHandler = () =>
-        resetGame(enhancedState, updateNextPieceDisplayFn)
+    const resetHandler = () =>
+        resetGame(enhancedState, updateNextPieceDisplayFn)
+    const restartHandler = resetHandler

23-28: Add window.tetrisGame debug object for consistency with memory-matrix.

Only the memory-matrix game currently exports a window debug object (window.memoryMatrixGame.getGame(), etc.) for development inspection. To align tetris with that pattern, expose a similar debug object in the game instance returned from initTetrisGame().

src/lib/server/db/client.ts (1)

7-15: Mixing process.env and import.meta.env in the same module.

isCi reads from process.env.CI (line 7) while dbUrl falls back to import.meta.env.TURSO_DATABASE_URL (line 11). In Astro's Vite-based build, process.env is only reliably available server-side at runtime, whereas import.meta.env is statically replaced at build time. This works for an SSR server module, but be aware that process.env.CI won't be available if this module is ever bundled for the client or processed by Vite's client transform.

src/lib/server/db/queries.ts (1)

113-126: Redundant try/catch — upsertUserStats already swallows all exceptions.

upsertUserStats (line 57-111) catches every error internally and returns false, so the catch block here is dead code. This is harmless but slightly misleading — readers may expect errors to propagate from upsertUserStats.

♻️ Simplified version (optional)
 export async function updateUserLevel(
     userId: string,
     newLevel: number
 ): Promise<boolean> {
-    try {
-        return await upsertUserStats(userId, { level: newLevel })
-    } catch (error) {
-        console.error('[updateUserLevel] Error:', sanitizeError(error))
-        return false
-    }
+    return upsertUserStats(userId, { level: newLevel })
 }
src/lib/services/challengeService.ts (1)

236-240: Duplicate JSDoc block.

Lines 236–240 contain two consecutive doc comments for checkAndUpdateStreak — one is a stale leftover.

♻️ Remove duplicate JSDoc
-/**
- * Check if all daily challenges are completed and update streak
- */
 /**
  * Check if all daily challenges are completed and update streak atomically
  */
 async function checkAndUpdateStreak(userId: string): Promise<void> {
src/lib/games/snake/SnakeRenderer.ts (1)

174-182: Satisfy Biome linter: avoid implicit return in forEach callback.

The arrow function graphic => graphic.destroy() implicitly returns the result of destroy(). Biome's useIterableCallbackReturn flags this for forEach.

Suggested fix
-        this.graphics.forEach(graphic => graphic.destroy())
+        this.graphics.forEach(graphic => { graphic.destroy() })
src/lib/games/snake/initFramework.ts (3)

141-154: Render loop duplicates rendering already done by onStateChange callback.

The onStateChange callback (line 66) already calls renderer.render(state) on every state change. This separate rAF loop renders the same state again before markRendered() clears needsRedraw. Consider removing renderer.render from the callback and relying solely on this loop, which already handles markRendered().

Suggested fix: remove render from callback
         onStateChange: state => {
-            renderer.render(state)
             updateUI(state as ReturnType<SnakeGame['getState']>)
             customCallbacks?.onStateChange?.(state)
         },

285-299: restartHandler and playAgainHandler are identical.

Both handlers call game.reset() and hide the overlay. Consider sharing a single handler function.

Suggested deduplication
-    const restartHandler = () => {
-        game.reset()
-        const overlay = document.getElementById('game-over-overlay')
-        if (overlay) {
-            overlay.classList.add('hidden')
-        }
-    }
-
-    const playAgainHandler = () => {
-        game.reset()
-        const overlay = document.getElementById('game-over-overlay')
-        if (overlay) {
-            overlay.classList.add('hidden')
-        }
-    }
+    const restartHandler = () => {
+        game.reset()
+        const overlay = document.getElementById('game-over-overlay')
+        if (overlay) {
+            overlay.classList.add('hidden')
+        }
+    }
+
+    const playAgainHandler = restartHandler

113-127: Unsafe casting of event data for achievement notifications.

The chain (event as { data: unknown }).data as { newAchievements?: ... } silently swallows structural mismatches. If the end event payload shape changes, this won't surface an error — it'll just skip notifications. Consider adding a lightweight guard instead of double-casting.

src/lib/games/core/ScoreManager.ts (1)

141-149: Non-OK responses are silently treated as failure.

When response.ok is false, the method returns { success: false } without logging the HTTP status or response body. This makes it hard to diagnose server-side issues (e.g., 401, 500).

Suggested improvement
             if (response.ok) {
                 const result = await response.json()
                 return {
                     success: true,
                     newAchievements: result.newAchievements,
                 }
             } else {
+                console.error(`[ScoreManager] Score save failed: ${response.status} ${response.statusText}`)
                 return { success: false }
             }

Comment thread src/lib/games/snake/initFramework.ts
Comment thread src/lib/games/snake/SnakeGame.ts
Comment thread src/lib/games/snake/SnakeRenderer.ts
- Add try/catch around renderer.initialize() with cleanup on failure

- Fix double rAF rendering by removing renderLoop and adding markRendered to onStateChange

- Fix double-destroy of gridGraphic in cleanup()

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@src/lib/games/snake/initFramework.ts`:
- Around line 53-68: The initFramework error path is inconsistent: instead of
throwing when renderer.initialize() fails, change it to log via the existing
handleGameError() and return undefined like the missing-container case so
callers only need to handle undefined; specifically, in the catch for
renderer.initialize() replace console.error(...) with handleGameError(...) and
perform the same cleanup (using renderer.getApp() and renderer.cleanup()) then
return undefined rather than rethrowing the error.

In `@src/lib/games/snake/SnakeRenderer.ts`:
- Around line 174-182: In clearGameObjects(), the arrow shorthand in
graphics.forEach(graphic => graphic.destroy()) implicitly returns the destroyed
object and triggers the lint rule; change the callback to a block body that does
not return a value (e.g., graphics.forEach(graphic => { graphic.destroy(); }))
or replace with a for...of loop to call graphic.destroy() for each item so the
forEach callback returns undefined.
🧹 Nitpick comments (5)
src/lib/games/snake/SnakeRenderer.ts (1)

200-219: responsive: false — confirm this is intentional for the Snake game.

The config hardcodes responsive: false, which means the canvas won't scale with the viewport. For a fixed-grid game this can be fine, but the coding guidelines call for handling responsive canvas sizing with proper scaling across devices. If this game will be played on mobile, you may need responsive scaling or at least viewport-aware cellSize computation.

src/lib/games/snake/initFramework.ts (4)

130-142: Fragile double type assertion on event payload.

The chain (event as { data: unknown }).data as { newAchievements?: ... } will silently produce undefined if the event shape ever changes. If the BaseGame.on('end', ...) callback has a defined signature, prefer typing the parameter directly. If not, consider adding a runtime guard before accessing .data.


281-295: restartHandler and playAgainHandler are identical — extract a shared helper.

Both handlers call game.reset() and hide the overlay. Consolidate into one function to reduce duplication.

Suggested refactor
-    const restartHandler = () => {
-        game.reset()
-        const overlay = document.getElementById('game-over-overlay')
-        if (overlay) {
-            overlay.classList.add('hidden')
-        }
-    }
-
-    const playAgainHandler = () => {
-        game.reset()
-        const overlay = document.getElementById('game-over-overlay')
-        if (overlay) {
-            overlay.classList.add('hidden')
-        }
-    }
+    const restartHandler = () => {
+        game.reset()
+        const overlay = document.getElementById('game-over-overlay')
+        if (overlay) {
+            overlay.classList.add('hidden')
+        }
+    }
+    const playAgainHandler = restartHandler

314-366: No touch/swipe controls for mobile.

The keyboard handler covers WASD and arrow keys, but there's no touch or swipe event handling. The coding guidelines require "proper touch and mouse event handling for mobile/desktop compatibility" and testing canvas functionality across devices. A swipe-to-direction handler would make the Snake game playable on touch devices.

As per coding guidelines: "Implement proper touch and mouse event handling for mobile/desktop compatibility" and "ensure mobile compatibility with touch/mouse event handling in games."


344-358: Pause toggle logic is duplicated with pauseHandler (lines 246-259).

The keyboard 'P' handler and the button click handler both implement the same pause/resume + button text update logic. Consider extracting a shared togglePause(game) helper to keep behavior in sync.

Comment thread src/lib/games/snake/initFramework.ts
Comment thread src/lib/games/snake/SnakeRenderer.ts
@vercel

vercel Bot commented Feb 7, 2026

Copy link
Copy Markdown

Deployment failed with the following error:

Creating the Deployment Timed Out.

@cwchanap

cwchanap commented Feb 7, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 53176069ab

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/lib/server/db/client.ts

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 25 out of 25 changed files in this pull request and generated 10 comments.

Comment thread src/lib/games/bubble-shooter/init.ts
Comment thread src/lib/games/core/errors.test.ts Outdated
Comment thread src/lib/games/core/errors.test.ts Outdated
Comment thread src/lib/games/core/errors.test.ts
Comment thread src/lib/games/core/errors.test.ts
Comment thread src/pages/snake/index.astro Outdated
Comment thread src/lib/server/db/queries.ts
Comment thread src/lib/server/db/client.ts
Comment thread src/lib/games/sudoku/game.test.ts
Comment thread src/lib/games/sudoku/game.test.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/lib/games/bubble-shooter/init.ts (2)

37-38: ⚠️ Potential issue | 🟡 Minor

Non-null assertions on getContext('2d') can cause silent NPEs.

getContext('2d') can return null (e.g., if the context is already acquired with a different type). Since you already guard the elements on line 33, add the same safety here.

🛡️ Proposed fix
-    const currentBubbleCtx = currentBubbleCanvas.getContext('2d')!
-    const nextBubbleCtx = nextBubbleCanvas.getContext('2d')!
+    const currentBubbleCtx = currentBubbleCanvas.getContext('2d')
+    const nextBubbleCtx = nextBubbleCanvas.getContext('2d')
+
+    if (!currentBubbleCtx || !nextBubbleCtx) {
+        throw new Error('Failed to get 2D rendering context')
+    }

251-269: ⚠️ Potential issue | 🔴 Critical

Replace window.dispatchEvent() with window.showAchievementAward().

The code dispatches an achievementsEarned CustomEvent, but no listeners exist for this event in the codebase, meaning achievement notifications won't display to users. The canonical pattern, defined in AchievementAward.astro and used in scoreService.ts, is to call window.showAchievementAward() directly when result.newAchievements is present. Update to match this pattern:

if (result.newAchievements && result.newAchievements.length > 0) {
    if (
        typeof window !== 'undefined' &&
        typeof window.showAchievementAward === 'function'
    ) {
        window.showAchievementAward(result.newAchievements)
    }
}
🤖 Fix all issues with AI agents
In `@src/lib/games/bubble-shooter/init.ts`:
- Around line 224-247: The cleanup function assigns null to renderer.app but
RendererState declares app as non-nullable, causing a TypeScript type error;
update the RendererState type to allow null (change app: Application to app:
Application | null) and keep the cleanup assignment, or alternatively remove the
renderer.app = null line inside cleanup and rely on cleanupCalled to prevent
re-execution—pick one approach and make sure any other code that reads
renderer.app checks for null if you choose the nullable type; reference symbols:
RendererState, renderer.app, cleanup (function), cleanupCalled.
🧹 Nitpick comments (14)
src/lib/server/db/client.ts (2)

7-12: Inconsistent env access: process.env vs import.meta.env.

Line 7 reads process.env.CI while line 12 reads import.meta.env.TURSO_DATABASE_URL. In Astro server-side code, import.meta.env includes all environment variables, so import.meta.env.CI would work here too. Consider using one accessor consistently for clarity.

Suggested change
-const isCi = process.env.CI === 'true' || process.env.CI === '1'
+const isCi = import.meta.env.CI === 'true' || import.meta.env.CI === '1'

7-16: CI fallback path is constructed but never verified to exist.

When running in CI with no TURSO_DATABASE_URL, the code falls back to db/db.sqlite relative to process.cwd(). If that file doesn't exist, the error will surface later (potentially as a cryptic libsql error) rather than at startup. A brief existence check or a clearer log message when using the fallback would improve debuggability.

src/lib/games/sudoku/game.test.ts (2)

117-188: Consider extracting a helper for finding given / non-given cells.

The nested loop pattern to locate a given or non-given cell is repeated five times across placeNumber and clearCell tests (lines 119–132, 147–160, 172–186, 200–210, 217–228). A small helper like findCell(state, predicate) would reduce duplication and improve readability.

♻️ Example helper extraction
// At the top of the describe block or in a local helper:
function findCell(
    state: GameState,
    predicate: (cell: GameState['grid']['cells'][number][number]) => boolean
): { row: number; col: number } | null {
    for (let r = 0; r < 9; r++) {
        for (let c = 0; c < 9; c++) {
            if (predicate(state.grid.cells[r][c])) {
                return { row: r, col: c }
            }
        }
    }
    return null
}

// Usage in tests:
const given = findCell(state, (cell) => cell.isGiven)
expect(given).not.toBeNull()
selectCell(state, given!.row, given!.col)

284-301: calculateScore tests rely on implementation-specific magic numbers.

Line 299 asserts score >= 150 for easy difficulty, which tightly couples this test to the internal scoring formula. If the scoring weights change, this test breaks without an obvious reason. Consider either importing/referencing the difficulty bonus constants or asserting a relative property (e.g., score > 0) to make the test more resilient.

Also, both easyState and hardState on line 293 are freshly initialized (unsolved) puzzles — the assertion that hardState.score > easyState.score only validates the difficulty multiplier, not actual gameplay scoring. A brief comment clarifying this intent would help future readers.

src/lib/games/bubble-shooter/init.ts (4)

22-24: Avoid any for the return type and stats parameter.

Promise<any> loses all type safety on the returned game instance. Consider defining an interface (e.g., BubbleShooterGameInstance) for the return value. Similarly, stats: any on the callback should use a concrete type.

♻️ Suggested typing
+interface BubbleShooterGameInstance {
+  restart: () => void;
+  getState: () => typeof enhancedState;
+  endGame: () => void;
+  cleanup: () => void;
+}
+
-export async function initBubbleShooterGame(callbacks?: {
-    onGameOver?: (finalScore: number, stats: any) => void
-}): Promise<any> {
+export async function initBubbleShooterGame(callbacks?: {
+    onGameOver?: (finalScore: number, stats: GameOverStats) => void
+}): Promise<BubbleShooterGameInstance> {

As per coding guidelines, "Use type annotations for function parameters in TypeScript" and "Implement proper game state management with TypeScript types."


136-168: Duplicate handler logic for resetBtn and restartBtn.

Lines 136-151 and 153-168 are identical. Extract a shared handler to reduce duplication.

♻️ Proposed refactor
+    const handleReset = () => {
+        const gameOverOverlay = document.getElementById('game-over-overlay')
+        if (gameOverOverlay) {
+            gameOverOverlay.classList.add('hidden')
+        }
+        resetGame(enhancedState, updateCurrentBubbleDisplayFn, updateNextBubbleDisplayFn)
+    }
+
-    resetBtn?.addEventListener('click', () => { /* ... */ }, { signal })
-    restartBtn?.addEventListener('click', () => { /* ... */ }, { signal })
+    resetBtn?.addEventListener('click', handleReset, { signal })
+    restartBtn?.addEventListener('click', handleReset, { signal })

214-248: Missing debug object export per coding guidelines.

The returned game instance doesn't expose a debug object on window. Other games expose window.<gameName>Game.getGame() for development inspection.

♻️ Suggested addition

Add after line 212, before the return:

if (import.meta.env.DEV) {
    (window as any).bubbleShooterGame = {
        getGame: () => enhancedState,
    }
}

As per coding guidelines: "Use game debug objects window.gameNameGame.getGame() for development inspection and debugging."


178-188: No touch event listeners for mobile compatibility.

Only mousemove and click are registered on the canvas. For mobile devices, consider also handling touchmove and touchstart/touchend (or using pointer events for unified handling).

As per coding guidelines: "Implement proper touch and mouse event handling for mobile/desktop compatibility" and "Test canvas functionality across devices (mobile/desktop) and ensure mobile compatibility."

src/lib/games/snake/SnakeRenderer.ts (1)

174-184: Consider object pooling for per-frame graphics allocation.

Every redraw creates new PIXI.Graphics objects for all snake segments and food, then destroys them on the next frame. For a snake game the object count is small so this is fine, but if you plan to extend this pattern to games with many more objects, a graphics pool would reduce GC pressure.

src/lib/games/snake/initFramework.ts (5)

130-143: Fragile event data casting.

The onGameEnd handler uses a chain of unsafe casts to extract newAchievements from the event. If the 'end' event emitted by BaseGame doesn't include a .data.newAchievements property, this silently does nothing (which is acceptable), but the double cast on line 132 ((event as { data: unknown }).data as { ... }) is brittle and would benefit from defensive narrowing.

Suggested safer narrowing
     const onGameEnd = (event: unknown) => {
-        const data = (event as { data: unknown }).data as {
-            newAchievements?: AchievementNotification[]
-        }
-        if (data?.newAchievements && data.newAchievements.length > 0) {
+        const eventObj = event as Record<string, unknown> | undefined
+        const data = eventObj?.data as Record<string, unknown> | undefined
+        const newAchievements = data?.newAchievements as AchievementNotification[] | undefined
+        if (newAchievements && newAchievements.length > 0) {
             if (
                 typeof window !== 'undefined' &&
                 typeof window.showAchievementAward === 'function'
             ) {
-                window.showAchievementAward(data.newAchievements)
+                window.showAchievementAward(newAchievements)
             }
         }
     }

155-171: Render loop runs unconditionally — even after game over.

The startRenderLoop kicks off a perpetual requestAnimationFrame cycle that never stops on its own. Even after game-over, it continues polling game.getState() every frame. While the needsRedraw check makes it a cheap no-op, this is unnecessary work. Consider stopping the loop when the game ends and restarting it on reset/start.


308-322: restartHandler and playAgainHandler are identical — extract a shared handler.

Both handlers call game.reset() and hide the overlay. This duplicated logic can be consolidated.

Suggested deduplication
-    const restartHandler = () => {
-        game.reset()
-        const overlay = document.getElementById('game-over-overlay')
-        if (overlay) {
-            overlay.classList.add('hidden')
-        }
-    }
-
-    const playAgainHandler = () => {
-        game.reset()
-        const overlay = document.getElementById('game-over-overlay')
-        if (overlay) {
-            overlay.classList.add('hidden')
-        }
-    }
+    const restartOrPlayAgainHandler = () => {
+        game.reset()
+        const overlay = document.getElementById('game-over-overlay')
+        if (overlay) {
+            overlay.classList.add('hidden')
+        }
+    }

     startBtn?.addEventListener('click', startHandler)
     endBtn?.addEventListener('click', endHandler)
     pauseBtn?.addEventListener('click', pauseHandler)
     resetBtn?.addEventListener('click', resetHandler)
-    restartBtn?.addEventListener('click', restartHandler)
-    playAgainBtn?.addEventListener('click', playAgainHandler)
+    restartBtn?.addEventListener('click', restartOrPlayAgainHandler)
+    playAgainBtn?.addEventListener('click', restartOrPlayAgainHandler)

Update the cleanup function similarly.


288-306: Unnecessary re-querying of DOM elements inside resetHandler.

startBtn and endBtn are already captured in the outer scope on lines 263–264. The inner declarations on lines 290–295 shadow them for no reason. Reuse the outer references.

Suggested fix
     const resetHandler = () => {
         game.reset()
-        const startBtn = document.getElementById(
-            'start-btn'
-        ) as HTMLButtonElement | null
-        const endBtn = document.getElementById(
-            'end-btn'
-        ) as HTMLButtonElement | null
         if (startBtn) {
             startBtn.style.display = 'inline-flex'
         }

55-69: Minor inconsistency: line 68 uses raw console.error instead of handleGameError.

The primary catch on line 56 correctly uses handleGameError, but the nested cleanup catch on line 68 falls back to console.error. For consistency, use handleGameError here too, though this is a low-severity nit since it's a last-resort cleanup path.

Comment thread src/lib/games/bubble-shooter/init.ts

This branch was successfully deployed

1 active deployment
Preview — f8f01041 Deployed Feb 8, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants