Skip to content

Tower defense game#424

Draft
amilich wants to merge 1 commit intomainfrom
cursor/tower-defense-game-513b
Draft

Tower defense game#424
amilich wants to merge 1 commit intomainfrom
cursor/tower-defense-game-513b

Conversation

@amilich
Copy link
Owner

@amilich amilich commented Feb 13, 2026

Implement a new standalone tower defense game accessible at /tower-defense.


Open in Cursor Open in Web


Open with Devin

Co-authored-by: Andrew Milich <milichab@gmail.com>
@cursor
Copy link

cursor bot commented Feb 13, 2026

Cursor Agent can help with this pull request. Just @cursor in comments and I'll start working on changes in this branch.
Learn more about Cursor Agents

@assert-app
Copy link

assert-app bot commented Feb 13, 2026

Your pull request is now ready for review with Assert.

Open Review →


Stop waiting for your code to break. Ship with confidence using Assert.

@vercel
Copy link
Contributor

vercel bot commented Feb 13, 2026

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

Project Deployment Actions Updated (UTC)
isometric-city Ready Ready Preview, Comment Feb 13, 2026 7:52pm

Request Review

Copy link
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 2 potential issues.

View 6 additional findings in Devin Review.

Open in Devin Review

Comment on lines +70 to +72
const TILE_PATH: Set<string> = new Set(
PATH_CELLS.map((cell) => `${cell.x},${cell.y}`)
);
Copy link
Contributor

Choose a reason for hiding this comment

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

🔴 TILE_PATH only contains waypoint cells, not intermediate path cells — towers can be placed on the path

TILE_PATH is built from PATH_CELLS which only lists the 7 waypoint corners of the path (e.g. (0,2), (3,2), (3,7), etc.). The isOnPath() function checks against this set, but the actual enemy path includes all intermediate cells between waypoints.

Root Cause and Impact

At line 70-72, TILE_PATH is constructed by mapping only the waypoint cells:

const TILE_PATH: Set<string> = new Set(
  PATH_CELLS.map((cell) => `${cell.x},${cell.y}`)
);

The path goes from (0,2)(3,2) horizontally, meaning cells (1,2) and (2,2) are on the enemy route. But TILE_PATH only has 7 entries (the waypoints), while the actual path covers 25 cells. The 18 intermediate cells are missing.

This causes two problems:

  1. Towers can be placed on the path (line 436): isOnPath(col, row) returns false for intermediate cells like (1,2), so the placement check passes and a tower is placed directly on the enemy route.
  2. Visual mismatch (line 183): Intermediate path cells are not highlighted with the yellow background, making the path appear as disconnected dots rather than a continuous route.

Placing towers on the path doesn't block enemies (they move by distance along the path line, not by grid cells), so the tower just wastes coins while enemies walk right through it.

Prompt for agents
In src/app/tower-defense/page.tsx, lines 70-72, TILE_PATH is built from only the waypoint cells in PATH_CELLS. It needs to include ALL cells along the path segments between waypoints. Replace the TILE_PATH construction with code that iterates over consecutive pairs of PATH_CELLS and adds every intermediate cell. For each pair of consecutive waypoints, if they share the same x coordinate, iterate over all y values between them; if they share the same y coordinate, iterate over all x values between them. Add each intermediate cell as a string 'x,y' to the Set. This will fix both the tower placement check in isOnPath() at line 133 and the visual path highlighting at line 183.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +500 to +505
requestAnimationFrame(gameLoop);
};

const handle = requestAnimationFrame(gameLoop);
return () => {
cancelAnimationFrame(handle);
Copy link
Contributor

Choose a reason for hiding this comment

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

🔴 Game loop continues running after component unmount due to uncancelled recursive requestAnimationFrame

The cleanup function in the game loop useEffect only cancels the initial requestAnimationFrame handle, but once the callback fires it schedules a new frame whose handle is never stored or cancelled.

Root Cause and Impact

At lines 492-508, the game loop is set up as:

const handle = requestAnimationFrame(gameLoop);
return () => {
  cancelAnimationFrame(handle);
};

Inside gameLoop (line 500), requestAnimationFrame(gameLoop) is called recursively, returning a new handle that is never stored. When the cleanup runs, cancelAnimationFrame(handle) cancels the original handle (which has already fired and is a no-op), while the most recently scheduled frame continues to fire. This causes the game loop to keep running after the component unmounts, calling setGameState and setHudTick on an unmounted component indefinitely.

Impact: Memory leak and wasted CPU cycles. On every frame after unmount, simulate() runs and React state setters are called on a stale component. If the user navigates away and back, multiple game loops will stack up.

The standard fix is to use a ref or a mutable cancelled flag that the cleanup sets to true, and check it at the top of gameLoop before scheduling the next frame.

Suggested change
requestAnimationFrame(gameLoop);
};
const handle = requestAnimationFrame(gameLoop);
return () => {
cancelAnimationFrame(handle);
frameHandle = requestAnimationFrame(gameLoop);
};
let frameHandle = requestAnimationFrame(gameLoop);
return () => {
cancelAnimationFrame(frameHandle);
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

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