Skip to content

Improved Gameplay#1

Open
Aasumas wants to merge 8 commits intoCryptoDragonLady:mainfrom
Aasumas:main
Open

Improved Gameplay#1
Aasumas wants to merge 8 commits intoCryptoDragonLady:mainfrom
Aasumas:main

Conversation

@Aasumas
Copy link
Copy Markdown

@Aasumas Aasumas commented Mar 16, 2026

Coconut now disappears when created allowing for longer gameplay
Added An AI Player
Allowed the user which fruit to be unlocked. Default to the first 4

Summary by CodeRabbit

  • New Features

    • AI auto-play with a Toggle AI control and auto-instantiated AI on page load.
    • Configurable "Max Drop" selector to limit available fruit types.
    • Resizable game canvas with responsive drop handling, final-score display, and restart button.
    • Expanded next-fruit queue and longer visible fruit evolution guide; larger visual effect for final-tier merges.
  • Style

    • Updated layout and styling for controls, game area, and canvas container; subtle AI status indicator animation.
  • Chores

    • Added CI workflows for Android APK build and GitHub Pages deploy; README web-start links updated.

Coconut now disappears when created allowing for longer gameplay
Added An AI Player
Allowed the user which fruit to be unlocked. Default to the first 4
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 16, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds an AutoPlayer AI controller with UI toggle and decision loop, exposes game state and responsive canvas resizing, introduces Max Drop UI and dropPoolCap, changes merge/final-fruit behavior and visuals, adds CSS for a resizable canvas, workspace file, and CI workflows.

Changes

Cohort / File(s) Summary
AI Controller
FruitGameAndroid/www/ai.js, ai.js, fruit-deep-ai/ai.js
New AutoPlayer class: UI toggle, start/stop loop, makeDecision, calculateBestDropPosition, simulateDrop, executeDrop, and global window.aiPlayer initialization. Integrates with window.game state.
Game logic / AI surface
FruitGameAndroid/www/script.js, script.js, fruit-deep-ai/script.js
Adds setupResizeObserver, handleResize, getGameState, dropPoolCap; updates generateNextFruit to respect dropPoolCap; changes restartGame, mergeFruits behavior for final-fruit merges; createMergeEffect(x,y,isBig=false) signature updated.
HTML / UI
FruitGameAndroid/www/index.html, index.html, fruit-deep-ai/index.html
Wraps canvas in .canvas-container, adds Max Drop select control, AI toggle button (#aiBtn) and AI status element, and includes ai.js script references.
Styling
FruitGameAndroid/www/style.css, style.css, fruit-deep-ai/style.css
Adds .canvas-container and .limit-container styles, resize constraints and custom WebKit resize handle, ensures canvas fills container, and styles controls/AI status.
CI / Project config
fruit.code-workspace, .github/workflows/android-build.yml, .github/workflows/deploy-pages.yml
Adds VS Code workspace file and two GitHub Actions workflows: Android APK build and GitHub Pages deploy.
New demo/site files
fruit-deep-ai/* (index.html, script.js, style.css, ai.js)
Adds a complete fruit game demo variant with its own AI wiring, UI, styles, and game implementation used by the new AI and controls.

Sequence Diagram

sequenceDiagram
    participant User
    participant UI as AI Button\n(aiBtn)
    participant AutoPlayer as AutoPlayer\n(AI)
    participant Game as FruitGame\n(Game)
    participant Canvas as Canvas/DOM

    User->>UI: click Toggle AI
    UI->>AutoPlayer: toggleAI()
    alt activate
        AutoPlayer->>AutoPlayer: startLoop() (setInterval)
    else deactivate
        AutoPlayer->>AutoPlayer: stopLoop() (clearInterval)
    end

    rect rgba(120,170,220,0.5)
    AutoPlayer->>Game: getGameState()
    Game-->>AutoPlayer: state
    AutoPlayer->>AutoPlayer: calculateBestDropPosition() / simulateDrop()
    end

    AutoPlayer->>Game: executeDrop(x)
    Game->>Canvas: render drop / mergeFruits()
    Game-->>AutoPlayer: confirm
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰
I hop and probe each pixel bright,
nudging fruits to land just right.
Heuristics hum and columns sing,
cascades glitter, merges bring—
tiny paws, a clever spring. 🍐✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'Improved Gameplay' is vague and generic, using a non-descriptive term that doesn't convey meaningful information about the specific changes in the changeset. Consider using a more specific title that highlights the main changes, such as 'Add AI player and unlock-limit controls' or 'Add AI automation and fruit type selection'.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with 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.

❤️ Share

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

Tip

CodeRabbit can use OpenGrep to find security vulnerabilities and bugs across 17+ programming languages.

OpenGrep is compatible with Semgrep configurations. Add an opengrep.yml or semgrep.yml configuration file to your project to enable OpenGrep analysis.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 12

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@ai.js`:
- Around line 128-168: The nested full-grid sweep in the decision routine (the
for loops using x0/x1/x2 and simulateDrop with steps 2/10/20 over nextFruits)
causes ~100k simulations; replace with a two-stage candidate pruning: do a
coarse pass (e.g., sample x positions with a large step like 20 or 25 for each
depth using simulateDrop) to score all coarse columns, keep only the top-K
candidates (introduce a maxCandidates variable, e.g., 5), then perform a local
refinement around each retained candidate with finer steps (e.g., ±step range
with step 2 or 5) to compute final scores and propagate them back into
score0/score1 aggregation; ensure you limit candidates per depth and
short-circuit when no valid simulateDrop results are found, using the existing
FRUITS[type].size radius logic and the same score-discounting (0.5/0.8) so
behavior remains consistent while drastically reducing simulateDrop calls.
- Around line 214-215: The simulation's death check uses landY < 120 which
doesn't match the game's top-edge rule; update the condition where landY is
checked (the branch that currently does "if (landY < 120) return { failed: true
};") to compute the fruit's top edge and reject when that top edge is at or
above the danger line — e.g. compute topEdge = landY - size / 2 (using the
existing landY and size variables or fruit.y and fruit.size if available) and
return failed when topEdge <= 60 so the same top-edge rule as the game is
enforced.
- Around line 225-250: The bonuses use (5 - type) which can be zero or negative
for higher types, turning intended rewards into penalties; in both the "Vertical
Potential Bonus" and "Predecessor Stacking Bonus" blocks (loops over inputFruits
that update score using alignmentFactor and (5 - type)), clamp the multiplier to
be non‑negative (e.g., replace (5 - type) with Math.max(5 - type, 0) or an
equivalent positive-only scaling) so score only increases for intended target
types and never becomes a penalty.

In `@FruitGameAndroid/www/ai.js`:
- Line 214: Replace hard-coded pixel constants (120, 450, 60 etc.) in ai.js with
the game's dynamic board/spawn/danger values: read the board height and
spawn/danger thresholds used by the game (the same variables or functions that
compute canvas size/spawnY/dangerZoneHeight) and use them in the checks around
the landY early-return and the checks at lines ~384-389; specifically update the
landY check (currently "if (landY < 120) return { failed: true }") and the other
comparisons to reference the game's spawnY/dangerZoneHeight/boardHeight
variables or accessor functions so the AI decision logic scales with dynamic
canvas sizing.
- Around line 36-39: The AI loop in startLoop → makeDecision currently does an
expensive exhaustive nested scan that calls simulateDrop many times each tick;
replace that with a pruned search: in makeDecision first perform a
coarse-grained candidate pass (e.g., sample every Nth cell or evaluate only a
reduced set of columns) to produce a small candidate list, then run a top-K beam
search (keep best K candidates) invoking simulateDrop only on those; add a
simple board-evaluation cache (keyed by a board hash) used by simulateDrop to
avoid re-evaluating identical states, and consider lowering frequency by tying
the loop to requestAnimationFrame or increasing actionDelay when heavy work is
needed. Ensure these changes are applied to the heavy scanning logic referenced
in makeDecision/simulateDrop (and related scanning code around the same block)
so the main-thread 200ms loop no longer runs ~100k simulations per move.
- Around line 225-250: The bonuses use a hardcoded (5 - type) multiplier which
can become negative for unlocked fruit types >=5; replace that with a derived
positive typePriority computed from the current active unlock range (e.g., using
the unlocked max/min indices or unlocked list length) and use it in both scoring
branches where score is increased (the two occurrences inside the "Vertical
Potential Bonus" loop and the "Predecessor Stacking Bonus" loop); ensure the
computed typePriority is >= 1 and reflects higher priority for lower/easier
fruits relative to the current unlock window instead of FRUITS.length or a magic
constant.

In `@FruitGameAndroid/www/script.js`:
- Around line 28-33: The next-fruit queue is seeded before dropPoolCap is
initialized causing NaN entries and potential crashes in updateNextFruit; fix
this by centralizing cap synchronization into a single helper (e.g.,
ensureDropPoolCap or syncDropPoolCap) that reads `#maxDropFruit` and sets
this.dropPoolCap, call that helper at the start of the constructor before
calling this.generateNextFruit(), and also call it before any queue rebuilds
(the restart/rebuild path around generateNextFruit calls referenced in the code
near lines 396-400) so the dropdown and actual drop pool stay in sync; update
references to generateNextFruit and updateNextFruit accordingly to assume
dropPoolCap is already valid.
- Around line 56-65: handleResize currently only re-clamps dropPosition but must
also re-clamp all existing fruits because Fruit.update can return early for
sleeping fruits; inside handleResize (after updating canvas size and before
updateDropLine) iterate this.fruits and for each fruit clamp fruit.x to
[fruit.radius, width - fruit.radius] and fruit.y to [fruit.radius, height -
fruit.radius], and assign those clamped values directly on the fruit so sleeping
fruits are corrected (don’t rely on Fruit.update), optionally clearing or
resetting any out-of-bounds velocities/state if necessary.

In `@FruitGameAndroid/www/style.css`:
- Around line 85-93: The .canvas-container CSS sets min-width: 320px which
causes overflow and mismatch with the internal buffer sizing in script.js;
remove or relax that min-width (e.g., drop it or set a small/removable min) and
ensure the container uses width: 100%/max-width: 90vw with box-sizing:
border-box so padding doesn't inflate layout; also update the small-screen
breakpoint rules (the section around lines 100-103) to override min-width and
use max-width: 100%/height: auto so the visual canvas matches the container
dimensions script.js reads.

In `@script.js`:
- Around line 28-33: The nextFruits queue is seeded before dropPoolCap is
initialized causing NaN and crashes; fix by centralizing cap synchronization
(read and set dropPoolCap from `#maxDropFruit`) into a single function (e.g.,
syncDropPoolCap) and call it before any queue rebuilds or calls to
generateNextFruit/updateNextFruit (including in the constructor and the restart
path around where nextFruits is rebuilt and at the code near lines 396-400).
Ensure generateNextFruit and updateNextFruit rely on the synchronized
dropPoolCap rather than assuming it exists.
- Around line 56-65: handleResize currently only re-clamps dropPosition; you
must also re-clamp all existing fruits so none end up outside the new canvas
bounds (sleeping fruits won't self-correct because Fruit.update() returns
early). After resizing the canvas in handleResize, iterate this.fruits and for
each fruit set fruit.x = clamp(radius, fruit.x, canvas.width - radius) and
fruit.y = clamp(radius, fruit.y, canvas.height - radius) (use the fruit's radius
or bounding size), modifying the fruit position directly rather than relying on
Fruit.update(); keep calling this.updateDropLine() as before.

In `@style.css`:
- Around line 85-93: The CSS min-width on .canvas-container is forcing overflow
on narrow phones and desync with the internal buffer sized in script.js; remove
or override min-width: 320px for small screens (and the same change applied
around the 100-103 block) by setting min-width to auto or unset and ensure
.canvas-container uses width: 100% / max-width: 90vw with box-sizing: border-box
via a media query for viewports <600px; also update script.js to base its
internal buffer size on the container's actual clientWidth/clientHeight (not the
CSS min-width) so displayed canvas and physics coordinates stay in sync.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 08ac9360-af62-4bd4-b09e-cb013f9994b1

📥 Commits

Reviewing files that changed from the base of the PR and between 7ba835c and 2eec782.

📒 Files selected for processing (9)
  • FruitGameAndroid/www/ai.js
  • FruitGameAndroid/www/index.html
  • FruitGameAndroid/www/script.js
  • FruitGameAndroid/www/style.css
  • ai.js
  • fruit.code-workspace
  • index.html
  • script.js
  • style.css

Comment thread ai.js
Comment on lines +128 to +168
// Depth 0: High resolution (step 2 for precision stacking)
for (let x0 = minX0; x0 <= maxX0; x0 += 2) {
const result0 = this.simulateDrop(x0, type0, fruits, canvasWidth, canvasHeight);
if (result0.failed) continue;

let score0 = result0.score;

// Depth 1: Medium resolution (step 10)
if (nextFruits.length > 1) {
let maxScore1 = -Infinity;
const type1 = nextFruits[1];
const r1 = FRUITS[type1].size / 2;
for (let x1 = r1; x1 <= canvasWidth - r1; x1 += 10) {
const result1 = this.simulateDrop(x1, type1, result0.fruits, canvasWidth, canvasHeight);
if (result1.failed) continue;

let score1 = result1.score;

// Depth 2: Low resolution (step 20)
if (nextFruits.length > 2) {
let maxScore2 = -Infinity;
const type2 = nextFruits[2];
const r2 = FRUITS[type2].size / 2;
for (let x2 = r2; x2 <= canvasWidth - r2; x2 += 20) {
const result2 = this.simulateDrop(x2, type2, result1.fruits, canvasWidth, canvasHeight);
if (!result2.failed && result2.score > maxScore2) {
maxScore2 = result2.score;
}
}
if (maxScore2 !== -Infinity) {
score1 += maxScore2 * 0.5; // Discount future rewards slightly
}
}

if (score1 > maxScore1) {
maxScore1 = score1;
}
}
if (maxScore1 !== -Infinity) {
score0 += maxScore1 * 0.8; // Discount earlier future rewards
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

This search explodes to ~100k simulations per decision.

On a 400px board, the 2px × 10px × 20px nested sweeps are roughly 175 × 35 × 18 simulateDrop() calls every 200ms, and each call reallocates/scans the board again. That will stall rendering and input on mobile. Use a coarse pass plus local refinement, or prune to a small set of candidate columns.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ai.js` around lines 128 - 168, The nested full-grid sweep in the decision
routine (the for loops using x0/x1/x2 and simulateDrop with steps 2/10/20 over
nextFruits) causes ~100k simulations; replace with a two-stage candidate
pruning: do a coarse pass (e.g., sample x positions with a large step like 20 or
25 for each depth using simulateDrop) to score all coarse columns, keep only the
top-K candidates (introduce a maxCandidates variable, e.g., 5), then perform a
local refinement around each retained candidate with finer steps (e.g., ±step
range with step 2 or 5) to compute final scores and propagate them back into
score0/score1 aggregation; ensure you limit candidates per depth and
short-circuit when no valid simulateDrop results are found, using the existing
FRUITS[type].size radius logic and the same score-discounting (0.5/0.8) so
behavior remains consistent while drastically reducing simulateDrop calls.

Comment thread ai.js Outdated
Comment on lines +214 to +215
if (landY < 120) return { failed: true }; // Death penalty

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Reject drops using the same top-edge rule as the game.

The real loss condition is based on fruit.y - size / 2 <= 60, but the simulation only checks landY < 120. Large fruits can still be scored as valid even when their top edge is already above the danger line.

🛠️ Suggested fix
-        if (landY < 120) return { failed: true }; // Death penalty
+        if (landY - dropRadius <= 60) return { failed: true }; // Match runtime danger-line geometry
📝 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.

Suggested change
if (landY < 120) return { failed: true }; // Death penalty
if (landY - dropRadius <= 60) return { failed: true }; // Match runtime danger-line geometry
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ai.js` around lines 214 - 215, The simulation's death check uses landY < 120
which doesn't match the game's top-edge rule; update the condition where landY
is checked (the branch that currently does "if (landY < 120) return { failed:
true };") to compute the fruit's top edge and reject when that top edge is at or
above the danger line — e.g. compute topEdge = landY - size / 2 (using the
existing landY and size variables or fruit.y and fruit.size if available) and
return failed when topEdge <= 60 so the same top-edge rule as the game is
enforced.

Comment thread ai.js
Comment on lines +225 to +250
// NEW: Vertical Potential Bonus (Rescue buried fruits)
// If we drop a fruit in the same vertical column as a matching fruit below us
for (const f of inputFruits) {
if (f.type === type && f.y > landY) {
const dx = Math.abs(f.x - x);
const colWidth = (dropRadius + f.size / 2) * 0.8; // Use slightly narrower column for precision
if (dx < colWidth) {
// We are in the same relative "column"
// Bonus scales by alignment and prioritizes smaller, harder-to-rescue fruits
const alignmentFactor = 1 - (dx / colWidth);
score += 600 * alignmentFactor * (5 - type);
}
}
}

// NEW: Predecessor Stacking Bonus (Strategic chaining)
// If we drop a fruit on top of a fruit that is the NEXT type in the cycle
// e.g. Cherry (0) on top of Strawberry (1)
for (const f of inputFruits) {
if (f.type === type + 1 && f.y > landY) {
const dx = Math.abs(f.x - x);
const colWidth = (dropRadius + f.size / 2) * 0.9;
if (dx < colWidth) {
const alignmentFactor = 1 - (dx / colWidth);
// Strong bonus for setting up a chain reaction
score += 400 * alignmentFactor * (5 - type);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

These late-game “bonuses” become penalties.

(5 - type) is already 0 for lemons and negative for oranges and above, so the AI starts discouraging the exact rescue/stacking patterns the comments say it should reward.

🛠️ Suggested fix
+        const priorityWeight = Math.max(1, 5 - type);
+
         // NEW: Vertical Potential Bonus (Rescue buried fruits)
         // If we drop a fruit in the same vertical column as a matching fruit below us
         for (const f of inputFruits) {
             if (f.type === type && f.y > landY) {
                 const dx = Math.abs(f.x - x);
                 const colWidth = (dropRadius + f.size / 2) * 0.8; // Use slightly narrower column for precision
                 if (dx < colWidth) {
                     // We are in the same relative "column"
                     // Bonus scales by alignment and prioritizes smaller, harder-to-rescue fruits
                     const alignmentFactor = 1 - (dx / colWidth);
-                    score += 600 * alignmentFactor * (5 - type); 
+                    score += 600 * alignmentFactor * priorityWeight;
                 }
             }
         }
 ...
         for (const f of inputFruits) {
             if (f.type === type + 1 && f.y > landY) {
                 const dx = Math.abs(f.x - x);
                 const colWidth = (dropRadius + f.size / 2) * 0.9;
                 if (dx < colWidth) {
                     const alignmentFactor = 1 - (dx / colWidth);
                     // Strong bonus for setting up a chain reaction
-                    score += 400 * alignmentFactor * (5 - type);
+                    score += 400 * alignmentFactor * priorityWeight;
                 }
             }
         }
📝 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.

Suggested change
// NEW: Vertical Potential Bonus (Rescue buried fruits)
// If we drop a fruit in the same vertical column as a matching fruit below us
for (const f of inputFruits) {
if (f.type === type && f.y > landY) {
const dx = Math.abs(f.x - x);
const colWidth = (dropRadius + f.size / 2) * 0.8; // Use slightly narrower column for precision
if (dx < colWidth) {
// We are in the same relative "column"
// Bonus scales by alignment and prioritizes smaller, harder-to-rescue fruits
const alignmentFactor = 1 - (dx / colWidth);
score += 600 * alignmentFactor * (5 - type);
}
}
}
// NEW: Predecessor Stacking Bonus (Strategic chaining)
// If we drop a fruit on top of a fruit that is the NEXT type in the cycle
// e.g. Cherry (0) on top of Strawberry (1)
for (const f of inputFruits) {
if (f.type === type + 1 && f.y > landY) {
const dx = Math.abs(f.x - x);
const colWidth = (dropRadius + f.size / 2) * 0.9;
if (dx < colWidth) {
const alignmentFactor = 1 - (dx / colWidth);
// Strong bonus for setting up a chain reaction
score += 400 * alignmentFactor * (5 - type);
const priorityWeight = Math.max(1, 5 - type);
// NEW: Vertical Potential Bonus (Rescue buried fruits)
// If we drop a fruit in the same vertical column as a matching fruit below us
for (const f of inputFruits) {
if (f.type === type && f.y > landY) {
const dx = Math.abs(f.x - x);
const colWidth = (dropRadius + f.size / 2) * 0.8; // Use slightly narrower column for precision
if (dx < colWidth) {
// We are in the same relative "column"
// Bonus scales by alignment and prioritizes smaller, harder-to-rescue fruits
const alignmentFactor = 1 - (dx / colWidth);
score += 600 * alignmentFactor * priorityWeight;
}
}
}
// NEW: Predecessor Stacking Bonus (Strategic chaining)
// If we drop a fruit on top of a fruit that is the NEXT type in the cycle
// e.g. Cherry (0) on top of Strawberry (1)
for (const f of inputFruits) {
if (f.type === type + 1 && f.y > landY) {
const dx = Math.abs(f.x - x);
const colWidth = (dropRadius + f.size / 2) * 0.9;
if (dx < colWidth) {
const alignmentFactor = 1 - (dx / colWidth);
// Strong bonus for setting up a chain reaction
score += 400 * alignmentFactor * priorityWeight;
}
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ai.js` around lines 225 - 250, The bonuses use (5 - type) which can be zero
or negative for higher types, turning intended rewards into penalties; in both
the "Vertical Potential Bonus" and "Predecessor Stacking Bonus" blocks (loops
over inputFruits that update score using alignmentFactor and (5 - type)), clamp
the multiplier to be non‑negative (e.g., replace (5 - type) with Math.max(5 -
type, 0) or an equivalent positive-only scaling) so score only increases for
intended target types and never becomes a penalty.

Comment on lines +36 to +39
startLoop() {
if (!this.intervalId) {
this.intervalId = setInterval(() => this.makeDecision(), this.actionDelay);
console.log("AI Player started");
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

This search is too expensive for a 200 ms main-thread loop.

On a ~360-400 px board, the 2/10/20 px nested scan is still roughly 100k simulateDrop() calls for one move, and each simulation does several full-board passes plus pairwise scoring. In the Android WebView this is likely to stall rendering and physics right when the board settles. Please prune the tree first: coarse-to-fine sampling, top-k beam search, or a cached board-hash evaluation would all be much safer here.

Also applies to: 128-168

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@FruitGameAndroid/www/ai.js` around lines 36 - 39, The AI loop in startLoop →
makeDecision currently does an expensive exhaustive nested scan that calls
simulateDrop many times each tick; replace that with a pruned search: in
makeDecision first perform a coarse-grained candidate pass (e.g., sample every
Nth cell or evaluate only a reduced set of columns) to produce a small candidate
list, then run a top-K beam search (keep best K candidates) invoking
simulateDrop only on those; add a simple board-evaluation cache (keyed by a
board hash) used by simulateDrop to avoid re-evaluating identical states, and
consider lowering frequency by tying the loop to requestAnimationFrame or
increasing actionDelay when heavy work is needed. Ensure these changes are
applied to the heavy scanning logic referenced in makeDecision/simulateDrop (and
related scanning code around the same block) so the main-thread 200ms loop no
longer runs ~100k simulations per move.

}
}

if (landY < 120) return { failed: true }; // Death penalty
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Derive the danger thresholds from the actual board size.

120, 450, and 60 are fixed pixels, but this PR also introduces dynamic canvas sizing. Once the board height changes, these checks stop matching the real lose zone, so the AI will reject safe drops on small boards and under-penalize tall stacks on large ones. Please source them from the same danger/spawn values the game uses instead of hard-coding them here.

Also applies to: 384-389

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@FruitGameAndroid/www/ai.js` at line 214, Replace hard-coded pixel constants
(120, 450, 60 etc.) in ai.js with the game's dynamic board/spawn/danger values:
read the board height and spawn/danger thresholds used by the game (the same
variables or functions that compute canvas size/spawnY/dangerZoneHeight) and use
them in the checks around the landY early-return and the checks at lines
~384-389; specifically update the landY check (currently "if (landY < 120)
return { failed: true }") and the other comparisons to reference the game's
spawnY/dangerZoneHeight/boardHeight variables or accessor functions so the AI
decision logic scales with dynamic canvas sizing.

Comment on lines +56 to +65
handleResize(width, height) {
// Update canvas internal resolution to match display size
this.canvas.width = width;
this.canvas.height = height;

// Clamp drop position to new width
this.dropPosition = Math.min(this.dropPosition, this.canvas.width - 25);
this.dropPosition = Math.max(25, this.dropPosition);

this.updateDropLine();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Resize needs to re-clamp existing fruit positions.

Only dropPosition is corrected here. If the user shrinks the canvas, fruits near the right or bottom edge can end up outside the new bounds, and sleeping fruits never self-correct because Fruit.update() returns early for them.

🛠️ Suggested fix
     handleResize(width, height) {
         // Update canvas internal resolution to match display size
         this.canvas.width = width;
         this.canvas.height = height;
+
+        for (const fruit of this.fruits) {
+            const radius = FRUITS[fruit.type].size / 2;
+            const maxX = Math.max(radius, width - radius);
+            const maxY = Math.max(radius, height - radius);
+            const clampedX = Math.min(Math.max(fruit.x, radius), maxX);
+            const clampedY = Math.min(Math.max(fruit.y, radius), maxY);
+
+            if (clampedX !== fruit.x || clampedY !== fruit.y) {
+                fruit.x = clampedX;
+                fruit.y = clampedY;
+                fruit.isSleeping = false;
+                fruit.stillFrames = 0;
+            }
+        }
         
         // Clamp drop position to new width
         this.dropPosition = Math.min(this.dropPosition, this.canvas.width - 25);
         this.dropPosition = Math.max(25, this.dropPosition);
         
         this.updateDropLine();
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@FruitGameAndroid/www/script.js` around lines 56 - 65, handleResize currently
only re-clamps dropPosition but must also re-clamp all existing fruits because
Fruit.update can return early for sleeping fruits; inside handleResize (after
updating canvas size and before updateDropLine) iterate this.fruits and for each
fruit clamp fruit.x to [fruit.radius, width - fruit.radius] and fruit.y to
[fruit.radius, height - fruit.radius], and assign those clamped values directly
on the fruit so sleeping fruits are corrected (don’t rely on Fruit.update),
optionally clearing or resetting any out-of-bounds velocities/state if
necessary.

Comment on lines +85 to +93
.canvas-container {
position: relative;
display: inline-block;
resize: both;
overflow: hidden;
min-width: 320px;
min-height: 480px;
max-width: 90vw;
max-height: 80vh;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

The new container/canvas sizing breaks small-screen play.

min-width: 320px can overflow narrow phones once the body and .game-container padding are applied, and under the existing <600px breakpoint the canvas still falls back to max-width: 350px/height: auto while script.js sizes the internal buffer from .canvas-container. That leaves clipping or coordinate drift between what the player sees and where drops/physics run.

Also applies to: 100-103

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@FruitGameAndroid/www/style.css` around lines 85 - 93, The .canvas-container
CSS sets min-width: 320px which causes overflow and mismatch with the internal
buffer sizing in script.js; remove or relax that min-width (e.g., drop it or set
a small/removable min) and ensure the container uses width: 100%/max-width: 90vw
with box-sizing: border-box so padding doesn't inflate layout; also update the
small-screen breakpoint rules (the section around lines 100-103) to override
min-width and use max-width: 100%/height: auto so the visual canvas matches the
container dimensions script.js reads.

Comment thread script.js Outdated
Comment thread script.js
Comment on lines +56 to +65
handleResize(width, height) {
// Update canvas internal resolution to match display size
this.canvas.width = width;
this.canvas.height = height;

// Clamp drop position to new width
this.dropPosition = Math.min(this.dropPosition, this.canvas.width - 25);
this.dropPosition = Math.max(25, this.dropPosition);

this.updateDropLine();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Resize needs to re-clamp existing fruit positions.

Only dropPosition is corrected here. If the user shrinks the canvas, fruits near the right or bottom edge can end up outside the new bounds, and sleeping fruits never self-correct because Fruit.update() returns early for them.

🛠️ Suggested fix
     handleResize(width, height) {
         // Update canvas internal resolution to match display size
         this.canvas.width = width;
         this.canvas.height = height;
+
+        for (const fruit of this.fruits) {
+            const radius = FRUITS[fruit.type].size / 2;
+            const maxX = Math.max(radius, width - radius);
+            const maxY = Math.max(radius, height - radius);
+            const clampedX = Math.min(Math.max(fruit.x, radius), maxX);
+            const clampedY = Math.min(Math.max(fruit.y, radius), maxY);
+
+            if (clampedX !== fruit.x || clampedY !== fruit.y) {
+                fruit.x = clampedX;
+                fruit.y = clampedY;
+                fruit.isSleeping = false;
+                fruit.stillFrames = 0;
+            }
+        }
         
         // Clamp drop position to new width
         this.dropPosition = Math.min(this.dropPosition, this.canvas.width - 25);
         this.dropPosition = Math.max(25, this.dropPosition);
         
         this.updateDropLine();
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@script.js` around lines 56 - 65, handleResize currently only re-clamps
dropPosition; you must also re-clamp all existing fruits so none end up outside
the new canvas bounds (sleeping fruits won't self-correct because Fruit.update()
returns early). After resizing the canvas in handleResize, iterate this.fruits
and for each fruit set fruit.x = clamp(radius, fruit.x, canvas.width - radius)
and fruit.y = clamp(radius, fruit.y, canvas.height - radius) (use the fruit's
radius or bounding size), modifying the fruit position directly rather than
relying on Fruit.update(); keep calling this.updateDropLine() as before.

Comment thread style.css
Comment on lines +85 to +93
.canvas-container {
position: relative;
display: inline-block;
resize: both;
overflow: hidden;
min-width: 320px;
min-height: 480px;
max-width: 90vw;
max-height: 80vh;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

The new container/canvas sizing breaks small-screen play.

min-width: 320px can overflow narrow phones once the body and .game-container padding are applied, and under the existing <600px breakpoint the canvas still falls back to max-width: 350px/height: auto while script.js sizes the internal buffer from .canvas-container. That leaves clipping or coordinate drift between what the player sees and where drops/physics run.

Also applies to: 100-103

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@style.css` around lines 85 - 93, The CSS min-width on .canvas-container is
forcing overflow on narrow phones and desync with the internal buffer sized in
script.js; remove or override min-width: 320px for small screens (and the same
change applied around the 100-103 block) by setting min-width to auto or unset
and ensure .canvas-container uses width: 100% / max-width: 90vw with box-sizing:
border-box via a media query for viewports <600px; also update script.js to base
its internal buffer size on the container's actual clientWidth/clientHeight (not
the CSS min-width) so displayed canvas and physics coordinates stay in sync.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

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)
script.js (1)

305-333: ⚠️ Potential issue | 🟠 Major

The new merge flash is painted in update(), so players never see it.

mergeFruits() calls createMergeEffect() before draw(), but draw() clears the canvas again at Line 435. The final-fruit effect is therefore erased before the frame is presented. Store effect state and render it from draw() for one or more frames instead of painting it directly here.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@script.js` around lines 305 - 333, mergeFruits currently calls
createMergeEffect which paints immediately and gets erased by draw()/update();
change createMergeEffect to set an effect state (e.g., this.pendingMergeEffect =
{x, y, isBig, framesLeft}) instead of drawing, decrement framesLeft each frame
in update()/draw(), and have draw() render any active pendingMergeEffect (using
isBig for radius/color) before finishing the frame; ensure mergeFruits still
sets the effect via this.createMergeEffect(...) and that the effect expires
after a short duration (e.g., N frames) so the flash is visible.
FruitGameAndroid/www/script.js (1)

305-333: ⚠️ Potential issue | 🟠 Major

The new merge flash is painted in update(), so players never see it.

mergeFruits() calls createMergeEffect() before draw(), but draw() clears the canvas again at Line 435. The final-fruit effect is therefore erased before the frame is presented. Store effect state and render it from draw() for one or more frames instead of painting it directly here.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@FruitGameAndroid/www/script.js` around lines 305 - 333, mergeFruits currently
calls createMergeEffect which paints immediately but the canvas is cleared later
in draw(), so the flash is never seen; change createMergeEffect to only set a
transient effect state (e.g., this.pendingMergeEffect or push an object into
this.mergeEffects with x, y, isBig, ttl/framesLeft) instead of drawing, then
modify draw() (or update()/draw pathway) to render active merge effects each
frame and decrement/remove their ttl so the flash persists for the desired
number of frames; update mergeFruits to still set the effect state when
newFruitType is final so the effect is rendered by draw() rather than being
painted directly.
♻️ Duplicate comments (4)
FruitGameAndroid/www/script.js (2)

56-65: ⚠️ Potential issue | 🟠 Major

Re-clamp existing fruits after a resize.

Only dropPosition is corrected here. Fruits already on the board can stay outside the new bounds, and sleeping fruits will never self-correct because Fruit.update() exits early for them.

Minimal fix
     handleResize(width, height) {
         // Update canvas internal resolution to match display size
         this.canvas.width = width;
         this.canvas.height = height;
+
+        for (const fruit of this.fruits) {
+            const radius = FRUITS[fruit.type].size / 2;
+            const maxX = Math.max(radius, width - radius);
+            const maxY = Math.max(radius, height - radius);
+            const clampedX = Math.min(Math.max(fruit.x, radius), maxX);
+            const clampedY = Math.min(Math.max(fruit.y, radius), maxY);
+
+            if (clampedX !== fruit.x || clampedY !== fruit.y) {
+                fruit.x = clampedX;
+                fruit.y = clampedY;
+                fruit.isSleeping = false;
+                fruit.stillFrames = 0;
+            }
+        }
         
         // Clamp drop position to new width
         this.dropPosition = Math.min(this.dropPosition, this.canvas.width - 25);
         this.dropPosition = Math.max(25, this.dropPosition);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@FruitGameAndroid/www/script.js` around lines 56 - 65, In handleResize, after
updating canvas size and clamping this.dropPosition, also iterate this.fruits
and re-clamp each fruit's horizontal position into the valid range (min 25, max
this.canvas.width - 25) so existing and sleeping fruits can't remain
out-of-bounds; do this directly on each fruit's x property (or equivalent)
rather than relying on Fruit.update (which exits early for sleeping fruits),
then call this.updateDropLine() as before.

28-33: ⚠️ Potential issue | 🟠 Major

Drop-cap sync is still incomplete.

Setting dropPoolCap before generateNextFruit() fixed the NaN path, but the queue is still built from hard-coded state instead of the current #maxDropFruit value. On first load/restart, the selected/default cap can disagree with nextFruits, and lowering the selector mid-game leaves already-queued fruits above the new limit. Please centralize the DOM sync, rebuild/clamp the queue on load/restart/change, and then refresh the preview.

Also applies to: 92-101, 396-400

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@FruitGameAndroid/www/script.js` around lines 28 - 33, The drop-cap handling
is inconsistent: set dropPoolCap (the instance field) from the `#maxDropFruit` DOM
selector and ensure all places that build the queue (the nextFruits
initialization and the other queue-construction sites around
generateNextFruit()) use that centralized value rather than a hard-coded
constant; implement a single sync function (e.g., syncDropCapFromDOM) that reads
`#maxDropFruit`, sets this.dropPoolCap, clamps existing this.nextFruits to the new
cap (removing or regenerating items above the limit), rebuilds/fills the queue
to length 3 using generateNextFruit() as needed, and calls the preview-refresh
routine so the UI always reflects the current cap on load, restart, and when the
selector changes (wire this sync function into initialization, restart handlers,
and the `#maxDropFruit` change event).
script.js (2)

28-33: ⚠️ Potential issue | 🟠 Major

Drop-cap sync is still incomplete.

Setting dropPoolCap before generateNextFruit() fixed the NaN path, but the queue is still built from hard-coded state instead of the current #maxDropFruit value. On first load/restart, the selected/default cap can disagree with nextFruits, and lowering the selector mid-game leaves already-queued fruits above the new limit. Please centralize the DOM sync, rebuild/clamp the queue on load/restart/change, and then refresh the preview.

Also applies to: 92-101, 396-400

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@script.js` around lines 28 - 33, The nextFruits queue is built from a
hard-coded size instead of the current DOM selector (`#maxDropFruit`) so it can
drift when the cap changes; update code to centralize DOM sync: read and set
dropPoolCap from the `#maxDropFruit` control before any calls to
generateNextFruit(), and replace every direct construction (the array at top and
the similar blocks at lines noted) with a single helper that rebuilds/clamps
nextFruits to exactly dropPoolCap by removing or trimming excess entries and
filling shortfall by calling generateNextFruit(); call that helper on load,
restart, and on selector change, then call the preview refresh function (e.g.,
refreshPreview()) so the UI matches the new queue.

56-65: ⚠️ Potential issue | 🟠 Major

Re-clamp existing fruits after a resize.

Only dropPosition is corrected here. Fruits already on the board can stay outside the new bounds, and sleeping fruits will never self-correct because Fruit.update() exits early for them.

Minimal fix
     handleResize(width, height) {
         // Update canvas internal resolution to match display size
         this.canvas.width = width;
         this.canvas.height = height;
+
+        for (const fruit of this.fruits) {
+            const radius = FRUITS[fruit.type].size / 2;
+            const maxX = Math.max(radius, width - radius);
+            const maxY = Math.max(radius, height - radius);
+            const clampedX = Math.min(Math.max(fruit.x, radius), maxX);
+            const clampedY = Math.min(Math.max(fruit.y, radius), maxY);
+
+            if (clampedX !== fruit.x || clampedY !== fruit.y) {
+                fruit.x = clampedX;
+                fruit.y = clampedY;
+                fruit.isSleeping = false;
+                fruit.stillFrames = 0;
+            }
+        }
         
         // Clamp drop position to new width
         this.dropPosition = Math.min(this.dropPosition, this.canvas.width - 25);
         this.dropPosition = Math.max(25, this.dropPosition);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@script.js` around lines 56 - 65, handleResize currently only clamps
this.dropPosition; you must also re-clamp all existing fruits so they cannot
remain outside the new canvas bounds (and sleeping fruits that skip Fruit.update
are handled). In handleResize (same function), loop over the collection of
fruits (e.g., this.fruits or the array that stores active Fruit instances) and
for each fruit clamp its x to the allowed range [25, this.canvas.width - 25];
update any derived state if needed (e.g., update drop-line or collision indices)
and then call this.updateDropLine() as before so the UI reflects the corrected
positions. Ensure you modify fruit.x directly (not relying on Fruit.update()) so
sleeping fruits are corrected immediately.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/android-build.yml:
- Around line 10-12: The environment variable FORCE_JAVASCRIPT_ACTIONS_TO_NODE24
is set but actions/setup-node@v4 is configured with node-version: '20', causing
a mismatch; either remove the env var or set the setup action's node-version to
'24'. Locate the workflow env block containing
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24 and the actions/setup-node@v4 step, then
update node-version to '24' to match the env variable (or delete the
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24 line if you intend to run Node 20) so the
runtime used by npm install and cordova build aligns with the environment flag.

---

Outside diff comments:
In `@FruitGameAndroid/www/script.js`:
- Around line 305-333: mergeFruits currently calls createMergeEffect which
paints immediately but the canvas is cleared later in draw(), so the flash is
never seen; change createMergeEffect to only set a transient effect state (e.g.,
this.pendingMergeEffect or push an object into this.mergeEffects with x, y,
isBig, ttl/framesLeft) instead of drawing, then modify draw() (or update()/draw
pathway) to render active merge effects each frame and decrement/remove their
ttl so the flash persists for the desired number of frames; update mergeFruits
to still set the effect state when newFruitType is final so the effect is
rendered by draw() rather than being painted directly.

In `@script.js`:
- Around line 305-333: mergeFruits currently calls createMergeEffect which
paints immediately and gets erased by draw()/update(); change createMergeEffect
to set an effect state (e.g., this.pendingMergeEffect = {x, y, isBig,
framesLeft}) instead of drawing, decrement framesLeft each frame in
update()/draw(), and have draw() render any active pendingMergeEffect (using
isBig for radius/color) before finishing the frame; ensure mergeFruits still
sets the effect via this.createMergeEffect(...) and that the effect expires
after a short duration (e.g., N frames) so the flash is visible.

---

Duplicate comments:
In `@FruitGameAndroid/www/script.js`:
- Around line 56-65: In handleResize, after updating canvas size and clamping
this.dropPosition, also iterate this.fruits and re-clamp each fruit's horizontal
position into the valid range (min 25, max this.canvas.width - 25) so existing
and sleeping fruits can't remain out-of-bounds; do this directly on each fruit's
x property (or equivalent) rather than relying on Fruit.update (which exits
early for sleeping fruits), then call this.updateDropLine() as before.
- Around line 28-33: The drop-cap handling is inconsistent: set dropPoolCap (the
instance field) from the `#maxDropFruit` DOM selector and ensure all places that
build the queue (the nextFruits initialization and the other queue-construction
sites around generateNextFruit()) use that centralized value rather than a
hard-coded constant; implement a single sync function (e.g., syncDropCapFromDOM)
that reads `#maxDropFruit`, sets this.dropPoolCap, clamps existing this.nextFruits
to the new cap (removing or regenerating items above the limit), rebuilds/fills
the queue to length 3 using generateNextFruit() as needed, and calls the
preview-refresh routine so the UI always reflects the current cap on load,
restart, and when the selector changes (wire this sync function into
initialization, restart handlers, and the `#maxDropFruit` change event).

In `@script.js`:
- Around line 28-33: The nextFruits queue is built from a hard-coded size
instead of the current DOM selector (`#maxDropFruit`) so it can drift when the cap
changes; update code to centralize DOM sync: read and set dropPoolCap from the
`#maxDropFruit` control before any calls to generateNextFruit(), and replace every
direct construction (the array at top and the similar blocks at lines noted)
with a single helper that rebuilds/clamps nextFruits to exactly dropPoolCap by
removing or trimming excess entries and filling shortfall by calling
generateNextFruit(); call that helper on load, restart, and on selector change,
then call the preview refresh function (e.g., refreshPreview()) so the UI
matches the new queue.
- Around line 56-65: handleResize currently only clamps this.dropPosition; you
must also re-clamp all existing fruits so they cannot remain outside the new
canvas bounds (and sleeping fruits that skip Fruit.update are handled). In
handleResize (same function), loop over the collection of fruits (e.g.,
this.fruits or the array that stores active Fruit instances) and for each fruit
clamp its x to the allowed range [25, this.canvas.width - 25]; update any
derived state if needed (e.g., update drop-line or collision indices) and then
call this.updateDropLine() as before so the UI reflects the corrected positions.
Ensure you modify fruit.x directly (not relying on Fruit.update()) so sleeping
fruits are corrected immediately.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 20114a77-4508-44ef-b867-b76dfc9956f7

📥 Commits

Reviewing files that changed from the base of the PR and between 164d51a and e90b84c.

📒 Files selected for processing (4)
  • .github/workflows/android-build.yml
  • .github/workflows/deploy-pages.yml
  • FruitGameAndroid/www/script.js
  • script.js

Comment on lines +10 to +12
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Workflow Node settings:"
rg -n --glob '.github/workflows/*.yml' 'FORCE_JAVASCRIPT_ACTIONS_TO_NODE24|node-version:' .github/workflows

echo
python - <<'PY'
import json
from pathlib import Path

for path in Path('.').rglob('package.json'):
    try:
        data = json.loads(path.read_text())
    except Exception as exc:
        print(f"== {path} ==\n<parse error: {exc}>\n")
        continue

    print(f"== {path} ==")
    print(json.dumps({
        "name": data.get("name"),
        "engines": data.get("engines"),
        "packageManager": data.get("packageManager")
    }, indent=2))
    print()
PY

Repository: CryptoDragonLady/fruit

Length of output: 441


Mismatched Node versions between environment variable and setup action.

FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true targets Node 24, but actions/setup-node@v4 is configured with node-version: '20'. The environment variable only affects JavaScript-based actions within the workflow; the actual build runtime (used by npm install and cordova build) is Node 20. Either update node-version to '24' to match the intended target, or remove the environment variable if Node 20 is the intended runtime.

Also applies to: 27-30

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/android-build.yml around lines 10 - 12, The environment
variable FORCE_JAVASCRIPT_ACTIONS_TO_NODE24 is set but actions/setup-node@v4 is
configured with node-version: '20', causing a mismatch; either remove the env
var or set the setup action's node-version to '24'. Locate the workflow env
block containing FORCE_JAVASCRIPT_ACTIONS_TO_NODE24 and the
actions/setup-node@v4 step, then update node-version to '24' to match the env
variable (or delete the FORCE_JAVASCRIPT_ACTIONS_TO_NODE24 line if you intend to
run Node 20) so the runtime used by npm install and cordova build aligns with
the environment flag.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (4)
fruit-deep-ai/index.html (1)

18-30: Max Drop options are limited to first 8 fruit types.

The dropdown provides options 0-7 (Cherry through Apple), but the FRUITS array contains 12 types (0-11). Players cannot select Peach (8), Mango (9), Pineapple (10), or Coconut (11) as the max drop cap. If this is intentional for game balance, consider adding a comment or tooltip explaining why.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@fruit-deep-ai/index.html` around lines 18 - 30, The maxDropFruit select
currently lists options 0–7 but the FRUITS array includes 12 entries (indices
0–11), so add options for indices 8–11 (Peach, Mango, Pineapple, Coconut) to the
<select id="maxDropFruit"> so players can choose those caps; update the option
elements to include value="8" through value="11" with the appropriate
emoji/labels or, if omission was intentional, add an inline comment or tooltip
near the maxDropFruit element explaining the design choice referencing the
FRUITS array.
fruit-deep-ai/ai.js (1)

362-365: Death check threshold doesn't match game's danger line.

Same issue as root ai.js: the simulation uses highestFruitY < 120 while the game triggers game-over at y = 60. Consider aligning thresholds for accurate move evaluation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@fruit-deep-ai/ai.js` around lines 362 - 365, The final death check uses a
threshold of 120 which doesn't match the game's danger line at y = 60; update
the comparison in the block that computes highestFruitY (using newFruits.reduce
and canvasHeight) so it compares against 60 (or a shared gameDangerY constant)
instead of 120 — i.e., change the condition if (highestFruitY < 120) to if
(highestFruitY < 60) or reference the canonical danger variable to keep move
evaluation consistent with the actual game-over line.
ai.js (1)

358-365: Death check threshold doesn't match game's danger line.

The game triggers game-over when a fruit's top edge crosses y = 60 (see checkGameOver() in script.js). The simulation uses highestFruitY < 120, which is a 60px buffer. While conservative, this mismatch could cause the AI to reject valid moves or fail to recognize actual danger accurately.

Consider aligning with the game's actual threshold:

-        if (highestFruitY < 120) return { failed: true };
+        if (highestFruitY <= 60) return { failed: true };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ai.js` around lines 358 - 365, The simulation uses a too-large death
threshold: compute highestFruitY (from newFruits) and compare it to the game's
actual danger line used in checkGameOver() instead of 120; replace the hardcoded
120 with the game's threshold (y = 60) or a single shared constant (e.g.,
GAME_OVER_Y or FRUIT_TOP_LIMIT) used by checkGameOver() so highestFruitY <
GAME_OVER_Y (or 60) is the condition for failure—update the check in the block
that computes highestFruitY and ensure the constant is imported/defined once to
keep simulation and game logic consistent.
fruit-deep-ai/style.css (1)

7-9: Remove unnecessary quotes around font-family name.

Per Stylelint rules, quotes around single-word font names like "Arial" are unnecessary.

♻️ Suggested fix
 body {
-    font-family: 'Arial', sans-serif;
+    font-family: Arial, sans-serif;
     background: linear-gradient(135deg, `#ff9a9e` 0%, `#fecfef` 50%, `#fecfef` 100%);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@fruit-deep-ai/style.css` around lines 7 - 9, The body selector's font-family
declaration uses unnecessary quotes around the single-word font name 'Arial';
update the font-family in the body rule (font-family: 'Arial', sans-serif) to
remove the quotes so it reads font-family: Arial, sans-serif to satisfy
Stylelint rules and avoid redundant quoting.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@fruit-deep-ai/ai.js`:
- Around line 231-259: The bonus multipliers use (5 - type) which can be zero or
negative for type >= 5; change both occurrences (the multiplier in the Vertical
Potential Bonus loop and the Predecessor Stacking Bonus loop that currently read
"*(5 - type)") to a non-negative clamp such as Math.max(0, 5 - type) (or
Math.max(1, 5 - type) if you want at least a small bonus) so high-type fruits
never turn the bonus into a penalty; update the two score adjustments that
reference (5 - type) accordingly.

In `@fruit-deep-ai/script.js`:
- Around line 306-320: When merging produces a higher-tier fruit (newFruitType),
update the game's progression state so future spawns can include that tier:
inside the merge branch where you handle non-final newFruit (the block that
creates new Fruit and pushes to this.fruits), set the progression variable
(maxUnlockedFruit) to at least newFruitType (e.g., maxUnlockedFruit =
Math.max(maxUnlockedFruit, newFruitType)) so generateNextFruit can spawn
unlocked tiers; reference the variables/methods newFruitType, maxUnlockedFruit,
generateNextFruit, Fruit, and this.fruits when applying the change.
- Around line 53-63: handleResize only adjusts dropPosition and not existing
fruit x positions, so when the canvas shrinks some fruits (especially sleeping
ones since Fruit.update returns early) can end up outside bounds; modify
handleResize to iterate the fruit collection (e.g., this.fruits) after resizing
and clamp each fruit.x to the allowed horizontal range (use the same 25px
padding logic: min(canvas.width-25, max(25, fruit.x))) and then call any
necessary per-fruit state updates (or mark them for rendering) so sleeping
fruits are corrected without relying on Fruit.update.

---

Nitpick comments:
In `@ai.js`:
- Around line 358-365: The simulation uses a too-large death threshold: compute
highestFruitY (from newFruits) and compare it to the game's actual danger line
used in checkGameOver() instead of 120; replace the hardcoded 120 with the
game's threshold (y = 60) or a single shared constant (e.g., GAME_OVER_Y or
FRUIT_TOP_LIMIT) used by checkGameOver() so highestFruitY < GAME_OVER_Y (or 60)
is the condition for failure—update the check in the block that computes
highestFruitY and ensure the constant is imported/defined once to keep
simulation and game logic consistent.

In `@fruit-deep-ai/ai.js`:
- Around line 362-365: The final death check uses a threshold of 120 which
doesn't match the game's danger line at y = 60; update the comparison in the
block that computes highestFruitY (using newFruits.reduce and canvasHeight) so
it compares against 60 (or a shared gameDangerY constant) instead of 120 — i.e.,
change the condition if (highestFruitY < 120) to if (highestFruitY < 60) or
reference the canonical danger variable to keep move evaluation consistent with
the actual game-over line.

In `@fruit-deep-ai/index.html`:
- Around line 18-30: The maxDropFruit select currently lists options 0–7 but the
FRUITS array includes 12 entries (indices 0–11), so add options for indices 8–11
(Peach, Mango, Pineapple, Coconut) to the <select id="maxDropFruit"> so players
can choose those caps; update the option elements to include value="8" through
value="11" with the appropriate emoji/labels or, if omission was intentional,
add an inline comment or tooltip near the maxDropFruit element explaining the
design choice referencing the FRUITS array.

In `@fruit-deep-ai/style.css`:
- Around line 7-9: The body selector's font-family declaration uses unnecessary
quotes around the single-word font name 'Arial'; update the font-family in the
body rule (font-family: 'Arial', sans-serif) to remove the quotes so it reads
font-family: Arial, sans-serif to satisfy Stylelint rules and avoid redundant
quoting.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d43c310f-6a7b-434a-b6cb-66b3a7dd43ab

📥 Commits

Reviewing files that changed from the base of the PR and between e90b84c and eedac8a.

📒 Files selected for processing (7)
  • FruitGameAndroid/www/script.js
  • README.md
  • ai.js
  • fruit-deep-ai/ai.js
  • fruit-deep-ai/index.html
  • fruit-deep-ai/script.js
  • fruit-deep-ai/style.css

Comment thread fruit-deep-ai/ai.js Outdated
Comment on lines +231 to +259
// NEW: Vertical Potential Bonus (Rescue buried fruits)
// If we drop a fruit in the same vertical column as a matching fruit below us
for (const f of inputFruits) {
if (f.type === type && f.y > landY) {
const dx = Math.abs(f.x - x);
const colWidth = (dropRadius + f.size / 2) * 0.8; // Use slightly narrower column for precision
if (dx < colWidth) {
// We are in the same relative "column"
// Bonus scales by alignment and prioritizes smaller, harder-to-rescue fruits
const alignmentFactor = 1 - (dx / colWidth);
score += 600 * alignmentFactor * (5 - type);
}
}
}

// NEW: Predecessor Stacking Bonus (Strategic chaining)
// If we drop a fruit on top of a fruit that is the NEXT type in the cycle
// e.g. Cherry (0) on top of Strawberry (1)
for (const f of inputFruits) {
if (f.type === type + 1 && f.y > landY) {
const dx = Math.abs(f.x - x);
const colWidth = (dropRadius + f.size / 2) * 0.9;
if (dx < colWidth) {
const alignmentFactor = 1 - (dx / colWidth);
// Strong bonus for setting up a chain reaction
score += 400 * alignmentFactor * (5 - type);
}
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

These late-game "bonuses" become penalties for higher fruit types.

Same issue as in root ai.js: the (5 - type) multiplier at lines 241 and 256 becomes zero or negative for types ≥ 5, turning intended bonuses into penalties.

🐛 Suggested fix
+        const priorityWeight = Math.max(1, 5 - type);
+
         // NEW: Vertical Potential Bonus (Rescue buried fruits)
         for (const f of inputFruits) {
             if (f.type === type && f.y > landY) {
                 const dx = Math.abs(f.x - x);
                 const colWidth = (dropRadius + f.size / 2) * 0.8;
                 if (dx < colWidth) {
                     const alignmentFactor = 1 - (dx / colWidth);
-                    score += 600 * alignmentFactor * (5 - type); 
+                    score += 600 * alignmentFactor * priorityWeight;
                 }
             }
         }

         // NEW: Predecessor Stacking Bonus
         for (const f of inputFruits) {
             if (f.type === type + 1 && f.y > landY) {
                 const dx = Math.abs(f.x - x);
                 const colWidth = (dropRadius + f.size / 2) * 0.9;
                 if (dx < colWidth) {
                     const alignmentFactor = 1 - (dx / colWidth);
-                    score += 400 * alignmentFactor * (5 - type);
+                    score += 400 * alignmentFactor * priorityWeight;
                 }
             }
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@fruit-deep-ai/ai.js` around lines 231 - 259, The bonus multipliers use (5 -
type) which can be zero or negative for type >= 5; change both occurrences (the
multiplier in the Vertical Potential Bonus loop and the Predecessor Stacking
Bonus loop that currently read "*(5 - type)") to a non-negative clamp such as
Math.max(0, 5 - type) (or Math.max(1, 5 - type) if you want at least a small
bonus) so high-type fruits never turn the bonus into a penalty; update the two
score adjustments that reference (5 - type) accordingly.

Comment thread fruit-deep-ai/script.js
Comment on lines +53 to +63
handleResize(width, height) {
// Update canvas internal resolution to match display size
this.canvas.width = width;
this.canvas.height = height;

// Clamp drop position to new width
this.dropPosition = Math.min(this.dropPosition, this.canvas.width - 25);
this.dropPosition = Math.max(25, this.dropPosition);

this.updateDropLine();
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Resize does not re-clamp existing fruit positions.

Same issue as in the Android variant: when the canvas shrinks, existing fruits near the edges may end up outside bounds. Sleeping fruits won't self-correct since Fruit.update() returns early.

🛠️ Suggested fix
     handleResize(width, height) {
         // Update canvas internal resolution to match display size
         this.canvas.width = width;
         this.canvas.height = height;
+
+        // Re-clamp existing fruits to new bounds
+        for (const fruit of this.fruits) {
+            const radius = FRUITS[fruit.type].size / 2;
+            fruit.x = Math.max(radius, Math.min(width - radius, fruit.x));
+            fruit.y = Math.max(radius, Math.min(height - radius, fruit.y));
+            fruit.isSleeping = false;
+            fruit.stillFrames = 0;
+        }
         
         // Clamp drop position to new width
         this.dropPosition = Math.min(this.dropPosition, this.canvas.width - 25);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@fruit-deep-ai/script.js` around lines 53 - 63, handleResize only adjusts
dropPosition and not existing fruit x positions, so when the canvas shrinks some
fruits (especially sleeping ones since Fruit.update returns early) can end up
outside bounds; modify handleResize to iterate the fruit collection (e.g.,
this.fruits) after resizing and clamp each fruit.x to the allowed horizontal
range (use the same 25px padding logic: min(canvas.width-25, max(25, fruit.x)))
and then call any necessary per-fruit state updates (or mark them for rendering)
so sleeping fruits are corrected without relying on Fruit.update.

Comment thread fruit-deep-ai/script.js
Comment on lines +306 to +320
// Create new merged fruit (only if not at max level)
if (fruit1.type < FRUITS.length - 1) {
const newFruitType = fruit1.type + 1;

// If it's the final fruit (Coconut), it "vanishes" immediately for points
if (newFruitType === FRUITS.length - 1) {
console.log("CONGRATULATIONS! Final fruit reached! 🥥");
this.createMergeEffect(mergeX, mergeY, true); // True for bigger effect
} else {
const newFruit = new Fruit(mergeX, mergeY, newFruitType, this.ctx);
newFruit.vx = (fruit1.vx + fruit2.vx) / 4; // Inherit some momentum
newFruit.vy = (fruit1.vy + fruit2.vy) / 4;
this.fruits.push(newFruit);
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Missing fruit unlock progression logic.

Unlike FruitGameAndroid/www/script.js (lines 305-309), this file doesn't advance maxUnlockedFruit when merging to higher tiers. This means generateNextFruit() will always be capped at the initial value of 2 (first 3 fruits), preventing natural progression.

🐛 Suggested fix
         // Create new merged fruit (only if not at max level)
         if (fruit1.type < FRUITS.length - 1) {
             const newFruitType = fruit1.type + 1;
+            
+            // Advance unlocked fruits if we hit a new tier
+            if (newFruitType > this.maxUnlockedFruit && newFruitType <= this.dropPoolCap) {
+                this.maxUnlockedFruit = newFruitType;
+                console.log(`NEW FRUIT UNLOCKED: ${FRUITS[newFruitType].emoji} (Index ${newFruitType})`);
+            }
             
             // If it's the final fruit (Coconut), it "vanishes" immediately for points
             if (newFruitType === FRUITS.length - 1) {
📝 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.

Suggested change
// Create new merged fruit (only if not at max level)
if (fruit1.type < FRUITS.length - 1) {
const newFruitType = fruit1.type + 1;
// If it's the final fruit (Coconut), it "vanishes" immediately for points
if (newFruitType === FRUITS.length - 1) {
console.log("CONGRATULATIONS! Final fruit reached! 🥥");
this.createMergeEffect(mergeX, mergeY, true); // True for bigger effect
} else {
const newFruit = new Fruit(mergeX, mergeY, newFruitType, this.ctx);
newFruit.vx = (fruit1.vx + fruit2.vx) / 4; // Inherit some momentum
newFruit.vy = (fruit1.vy + fruit2.vy) / 4;
this.fruits.push(newFruit);
}
}
// Create new merged fruit (only if not at max level)
if (fruit1.type < FRUITS.length - 1) {
const newFruitType = fruit1.type + 1;
// Advance unlocked fruits if we hit a new tier
if (newFruitType > this.maxUnlockedFruit && newFruitType <= this.dropPoolCap) {
this.maxUnlockedFruit = newFruitType;
console.log(`NEW FRUIT UNLOCKED: ${FRUITS[newFruitType].emoji} (Index ${newFruitType})`);
}
// If it's the final fruit (Coconut), it "vanishes" immediately for points
if (newFruitType === FRUITS.length - 1) {
console.log("CONGRATULATIONS! Final fruit reached! 🥥");
this.createMergeEffect(mergeX, mergeY, true); // True for bigger effect
} else {
const newFruit = new Fruit(mergeX, mergeY, newFruitType, this.ctx);
newFruit.vx = (fruit1.vx + fruit2.vx) / 4; // Inherit some momentum
newFruit.vy = (fruit1.vy + fruit2.vy) / 4;
this.fruits.push(newFruit);
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@fruit-deep-ai/script.js` around lines 306 - 320, When merging produces a
higher-tier fruit (newFruitType), update the game's progression state so future
spawns can include that tier: inside the merge branch where you handle non-final
newFruit (the block that creates new Fruit and pushes to this.fruits), set the
progression variable (maxUnlockedFruit) to at least newFruitType (e.g.,
maxUnlockedFruit = Math.max(maxUnlockedFruit, newFruitType)) so
generateNextFruit can spawn unlocked tiers; reference the variables/methods
newFruitType, maxUnlockedFruit, generateNextFruit, Fruit, and this.fruits when
applying the change.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
fruit-deep-ai/ai.js (1)

334-368: Consider extracting magic numbers as named constants.

Values like 120 (danger zone), 500 (height threshold), 5000 (burial penalty base), and 60 (distance to danger) are repeated or hardcoded. Extracting these as constants at the class or module level would improve readability and make tuning easier.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@fruit-deep-ai/ai.js` around lines 334 - 368, The code in the evaluation loop
for newFruits uses hardcoded "magic" numbers (120, 500, 5000, 60, 600, 2.5, 1.5,
0.9, 3) — extract these into named module-level constants (e.g., DANGER_ZONE_Y,
HEIGHT_THRESHOLD, BURIAL_PENALTY_BASE, DISTANCE_TO_DANGER_MIN,
HEIGHT_PENALTY_SCALE, HEIGHT_PENALTY_EXP, SIZE_GRADIENT_WEIGHT,
HOLE_OVERLAP_FACTOR, BURIAL_GROWTH_FACTOR) and replace the literals in the
functions and loops that reference newFruits, f1/f2 comparisons, the global
death penalty, and size-gradient logic; keep the same numeric values while
giving each a clear name so tuning via the constants is easy and readability of
expressions using score, FRUITS, and canvasWidth is improved.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@fruit-deep-ai/ai.js`:
- Around line 334-368: The code in the evaluation loop for newFruits uses
hardcoded "magic" numbers (120, 500, 5000, 60, 600, 2.5, 1.5, 0.9, 3) — extract
these into named module-level constants (e.g., DANGER_ZONE_Y, HEIGHT_THRESHOLD,
BURIAL_PENALTY_BASE, DISTANCE_TO_DANGER_MIN, HEIGHT_PENALTY_SCALE,
HEIGHT_PENALTY_EXP, SIZE_GRADIENT_WEIGHT, HOLE_OVERLAP_FACTOR,
BURIAL_GROWTH_FACTOR) and replace the literals in the functions and loops that
reference newFruits, f1/f2 comparisons, the global death penalty, and
size-gradient logic; keep the same numeric values while giving each a clear name
so tuning via the constants is easy and readability of expressions using score,
FRUITS, and canvasWidth is improved.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d2ba2c14-5555-4372-aeac-43ecb6689c5f

📥 Commits

Reviewing files that changed from the base of the PR and between eedac8a and 57ebddb.

📒 Files selected for processing (2)
  • fruit-deep-ai/ai.js
  • fruit-deep-ai/index.html
🚧 Files skipped from review as they are similar to previous changes (1)
  • fruit-deep-ai/index.html

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.

1 participant