Skip to content

🐛 Fix Kube Chess AI freeze and Kube Craft block selection#4110

Merged
clubanderson merged 2 commits intomainfrom
fix/broken-cards
Apr 1, 2026
Merged

🐛 Fix Kube Chess AI freeze and Kube Craft block selection#4110
clubanderson merged 2 commits intomainfrom
fix/broken-cards

Conversation

@clubanderson
Copy link
Copy Markdown
Collaborator

Summary

  • Kube Chess (Card: Kube Chess (kube_chess) #4083): AI gets stuck on "AI thinking..." indefinitely because the minimax search explores too many positions, blocking the main thread. Added position evaluation limit (50k positions), move ordering for better alpha-beta pruning, ref-based thinking guard to prevent effect re-triggering loops, and try/finally to ensure thinking state always resets.

  • Kube Craft (Card: Kube Craft (kube_craft) #4081): Block selection state not reflected during placement because mouse handlers used stale refs via useCallback with empty dependency arrays. Replaced ref-based indirection with proper useCallback dependencies on isErasing and selectedBlock state, and added canvas coordinate scaling for expanded mode.

Test plan

  • Open Arcade → Kube Chess, start a game, make a move — AI should respond within a few seconds (not freeze)
  • Try all difficulty levels (Easy, Medium, Hard) — AI should always respond
  • Open Arcade → Kube Craft, select different block types (stone, wood, etc.) and place — correct block type should appear
  • Select the eraser tool and click blocks — blocks should be removed, not replaced with grass
  • Expand Kube Craft card and verify clicks map to correct grid positions

Fixes #4083
Fixes #4081

Reverts #4103 (active user count in navbar) — feature explicitly
rejected by maintainer. Also disables the Auto-QA check that keeps
filing issues about this feature.

Signed-off-by: Andrew Anderson <andy@clubanderson.com>
KubeChess (#4083): AI gets stuck on "AI thinking..." indefinitely because
the minimax search can explore too many positions, blocking the main thread.
Fix adds a position evaluation counter (MAX_POSITIONS_EVALUATED = 50,000)
that bails out when exceeded, move ordering for better alpha-beta pruning,
a ref-based thinking guard to prevent effect re-triggering loops, and
try/finally to ensure thinking state always resets even on errors.

KubeCraft (#4081): Block selection state not reflected during placement
because mouse handlers used stale refs via useCallback with empty deps.
Fix replaces the ref-based indirection with proper useCallback dependencies
on isErasing and selectedBlock state, and adds canvas coordinate scaling
to handle CSS-scaled canvas in expanded mode.

Fixes #4083
Fixes #4081

Signed-off-by: Andrew Anderson <andy@clubanderson.com>
Copilot AI review requested due to automatic review settings April 1, 2026 12:22
@kubestellar-prow kubestellar-prow bot added the dco-signoff: yes Indicates the PR's author has signed the DCO. label Apr 1, 2026
@kubestellar-prow
Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please assign mikespreitzer for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 1, 2026

👋 Hey @clubanderson — thanks for opening this PR!

🤖 This project is developed exclusively using AI coding assistants.

Please do not attempt to code anything for this project manually.
All contributions should be authored using an AI coding tool such as:

This ensures consistency in code style, architecture patterns, test coverage,
and commit quality across the entire codebase.


This is an automated message.

@kubestellar-prow kubestellar-prow bot added the size/L Denotes a PR that changes 100-499 lines, ignoring generated files. label Apr 1, 2026
@clubanderson clubanderson merged commit 0b40979 into main Apr 1, 2026
16 of 18 checks passed
@kubestellar-prow kubestellar-prow bot deleted the fix/broken-cards branch April 1, 2026 12:22
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 1, 2026

Thank you for your contribution! Your PR has been merged.

Check out what's new:

Stay connected: Slack #kubestellar-dev | Multi-Cluster Survey

@netlify
Copy link
Copy Markdown

netlify bot commented Apr 1, 2026

Deploy Preview for kubestellarconsole ready!

Name Link
🔨 Latest commit cb87b10
🔍 Latest deploy log https://app.netlify.com/projects/kubestellarconsole/deploys/69cd0e0faf00f50008c41059
😎 Deploy Preview https://deploy-preview-4110.console-deploy-preview.kubestellar.io
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Fixes two Arcade game regressions in the Console frontend: Kube Chess AI hanging on “AI thinking…” and Kube Craft placing the wrong block due to stale handler state / incorrect coordinate mapping in expanded mode. Also updates Auto-QA workflow behavior around the “active user count” feature check.

Changes:

  • Kube Craft: reworks mouse handling to use stateful callbacks (no stale refs) and adds coordinate scaling for CSS-scaled canvas.
  • Kube Chess: adds a position-evaluation cap + move ordering to reduce minimax cost, and hardens “thinking” state transitions with a ref guard and try/finally.
  • Auto-QA: disables the “active user count feature” check step.

Reviewed changes

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

File Description
web/src/components/cards/KubeCraft.tsx Replaces ref-indirection with dependency-correct callbacks; adds grid coordinate conversion for scaled canvas.
web/src/components/cards/KubeChess.tsx Adds search limiting + move ordering and prevents AI “thinking” loop / stuck state via ref guard + try/finally.
.github/workflows/auto-qa.yml Disables an Auto-QA feature-check step for active user count.

Comment on lines +284 to +288
// Scale mouse coordinates to canvas logical size (handles CSS scaling in expanded mode)
const scaleX = CANVAS_WIDTH / rect.width
const scaleY = CANVAS_HEIGHT / rect.height
const x = Math.floor((e.clientX - rect.left) * scaleX / CELL_SIZE)
const y = Math.floor((e.clientY - rect.top) * scaleY / CELL_SIZE)
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

getGridCoords() divides by rect.width/rect.height; if either is 0 (or results in NaN), the bounds check won’t catch it (NaN comparisons are false) and placeBlock() can index world with NaN, throwing at runtime. Add a guard for rect.width/height <= 0 (and/or Number.isFinite(x/y)) before returning coords.

Suggested change
// Scale mouse coordinates to canvas logical size (handles CSS scaling in expanded mode)
const scaleX = CANVAS_WIDTH / rect.width
const scaleY = CANVAS_HEIGHT / rect.height
const x = Math.floor((e.clientX - rect.left) * scaleX / CELL_SIZE)
const y = Math.floor((e.clientY - rect.top) * scaleY / CELL_SIZE)
// If the canvas has no size, we cannot reliably map mouse coordinates
if (rect.width <= 0 || rect.height <= 0) {
return null
}
// Scale mouse coordinates to canvas logical size (handles CSS scaling in expanded mode)
const scaleX = CANVAS_WIDTH / rect.width
const scaleY = CANVAS_HEIGHT / rect.height
if (!Number.isFinite(scaleX) || !Number.isFinite(scaleY)) {
return null
}
const rawX = (e.clientX - rect.left) * scaleX / CELL_SIZE
const rawY = (e.clientY - rect.top) * scaleY / CELL_SIZE
if (!Number.isFinite(rawX) || !Number.isFinite(rawY)) {
return null
}
const x = Math.floor(rawX)
const y = Math.floor(rawY)

Copilot uses AI. Check for mistakes.
Comment on lines +284 to +289
// Scale mouse coordinates to canvas logical size (handles CSS scaling in expanded mode)
const scaleX = CANVAS_WIDTH / rect.width
const scaleY = CANVAS_HEIGHT / rect.height
const x = Math.floor((e.clientX - rect.left) * scaleX / CELL_SIZE)
const y = Math.floor((e.clientY - rect.top) * scaleY / CELL_SIZE)

Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

Coordinate scaling here assumes the canvas content is stretched to fill getBoundingClientRect() (independent scaleX/scaleY). In expanded mode the canvas uses objectFit: 'contain', which can introduce letterboxing when the container isn’t square; clicks in padded areas (or near edges) will map to the wrong grid cell. Consider computing a uniform scale (min of x/y), plus x/y offsets for the contained draw area, and ignore clicks in the padding.

Suggested change
// Scale mouse coordinates to canvas logical size (handles CSS scaling in expanded mode)
const scaleX = CANVAS_WIDTH / rect.width
const scaleY = CANVAS_HEIGHT / rect.height
const x = Math.floor((e.clientX - rect.left) * scaleX / CELL_SIZE)
const y = Math.floor((e.clientY - rect.top) * scaleY / CELL_SIZE)
const canvasAspect = CANVAS_WIDTH / CANVAS_HEIGHT
const rectAspect = rect.width / rect.height
// Compute the actual drawn content area within the canvas rect when objectFit: 'contain' is used.
let drawnWidth: number
let drawnHeight: number
let offsetX: number
let offsetY: number
if (rectAspect > canvasAspect) {
// Limited by height: full height used, width letterboxed
drawnHeight = rect.height
drawnWidth = rect.height * canvasAspect
offsetX = (rect.width - drawnWidth) / 2
offsetY = 0
} else {
// Limited by width: full width used, height letterboxed
drawnWidth = rect.width
drawnHeight = rect.width / canvasAspect
offsetX = 0
offsetY = (rect.height - drawnHeight) / 2
}
const mouseX = e.clientX - rect.left - offsetX
const mouseY = e.clientY - rect.top - offsetY
// Ignore clicks in the letterboxed padding outside the drawn content
if (mouseX < 0 || mouseY < 0 || mouseX > drawnWidth || mouseY > drawnHeight) {
return null
}
// Uniform scale from drawn pixels to logical canvas pixels
const scale = CANVAS_WIDTH / drawnWidth
const logicalX = mouseX * scale
const logicalY = mouseY * scale
const x = Math.floor(logicalX / CELL_SIZE)
const y = Math.floor(logicalY / CELL_SIZE)

Copilot uses AI. Check for mistakes.
counter.count++

// Bail out if we've evaluated too many positions to prevent UI freeze
if (counter.count > MAX_POSITIONS_EVALUATED) {
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

The position limit check is slightly off-by-one: counter is incremented before the check, and both this guard and the root-loop guard in findBestMove() use > rather than >=. As a result the search can exceed MAX_POSITIONS_EVALUATED (and still do an extra minimax call from the root) instead of enforcing a hard cap. Use >= (and/or check before increment) consistently to keep the limit strict.

Suggested change
if (counter.count > MAX_POSITIONS_EVALUATED) {
if (counter.count >= MAX_POSITIONS_EVALUATED) {

Copilot uses AI. Check for mistakes.
Comment on lines 2354 to +2362
# === ACTIVE USER COUNT FEATURE (every run) ===
- name: "Check: Active user count feature"
- name: "Check: Active user count feature (PERMANENTLY DISABLED)"
id: focus_user_count
working-directory: web
continue-on-error: true
run: |
echo "Checking active user count feature implementation..."
ISSUES=""

# Find user count component/feature
USER_COUNT_FILES=$(find src -name "*user*count*" -o -name "*active*user*" -o -name "*UserCount*" -o -name "*ActiveUser*" 2>/dev/null | grep "\.tsx\|\.ts" || true)
USER_COUNT_HOOKS=$(grep -rln "activeUsers\|userCount\|onlineUsers\|connectedUsers" src/ --include="*.tsx" --include="*.ts" 2>/dev/null || true)

if [ -z "$USER_COUNT_FILES" ] && [ -z "$USER_COUNT_HOOKS" ]; then
ISSUES="${ISSUES}### No active user count feature found\nConsider adding a real-time user count for console.kubestellar.io\n\n"
else
# Check for WebSocket or real-time updates
HAS_REALTIME=$(grep -rln "WebSocket\|socket\|useSubscription\|onmessage\|EventSource\|sse" src/ --include="*.tsx" --include="*.ts" 2>/dev/null | head -1 || true)
if [ -z "$HAS_REALTIME" ]; then
ISSUES="${ISSUES}### User count may not update in real-time\nConsider using WebSocket for live user count updates\n\n"
fi

# Check for demo mode handling in user count
USER_COUNT_DEMO=$(grep -rln "activeUser\|userCount" src/ --include="*.tsx" 2>/dev/null | while read f; do
HAS_DEMO=$(grep -l "demo\|Demo\|mock\|Mock" "$f" 2>/dev/null || true)
if [ -z "$HAS_DEMO" ]; then
basename "$f"
fi
done | head -5 || true)

if [ -n "$USER_COUNT_DEMO" ]; then
ISSUES="${ISSUES}### User count components without demo mode handling\n\`\`\`\n${USER_COUNT_DEMO}\n\`\`\`\n\n"
fi

# Check for error handling in user count
USER_COUNT_ERROR=$(grep -rln "activeUser\|userCount" src/ --include="*.tsx" 2>/dev/null | while read f; do
HAS_ERROR=$(grep -l "error\|Error\|catch\|fallback" "$f" 2>/dev/null || true)
if [ -z "$HAS_ERROR" ]; then
basename "$f"
fi
done | head -5 || true)

if [ -n "$USER_COUNT_ERROR" ]; then
ISSUES="${ISSUES}### User count without error handling\nShow fallback when count unavailable:\n\`\`\`\n${USER_COUNT_ERROR}\n\`\`\`\n\n"
fi

# Check for proper cleanup (unsubscribe)
SUBSCRIBE_COUNT=$(grep -rc "subscribe\|addEventListener\|onmessage" src/ --include="*user*" --include="*User*" 2>/dev/null | awk -F: '{sum+=$2} END {print sum}' || echo "0")
UNSUBSCRIBE_COUNT=$(grep -rc "unsubscribe\|removeEventListener\|close()" src/ --include="*user*" --include="*User*" 2>/dev/null | awk -F: '{sum+=$2} END {print sum}' || echo "0")

if [ "$SUBSCRIBE_COUNT" -gt 0 ] && [ "$UNSUBSCRIBE_COUNT" -eq 0 ]; then
ISSUES="${ISSUES}### User count subscriptions without cleanup\nEnsure WebSocket/EventSource connections are closed on unmount\n\n"
fi

# Check if user count is visible in header/footer
HEADER_USER=$(grep -rln "activeUser\|userCount\|UserCount" src/components/layout --include="*.tsx" src/components/header --include="*.tsx" src/components/Header* 2>/dev/null | head -1 || true)
if [ -z "$HEADER_USER" ]; then
ISSUES="${ISSUES}### User count not in header/layout\nConsider showing active users in the app header for visibility\n\n"
fi
fi

if [ -n "$ISSUES" ]; then
echo "found=true" >> "$GITHUB_OUTPUT"
printf "%b" "$ISSUES" > /tmp/focus-user-count.txt
echo "User count issues found"
else
echo "found=false" >> "$GITHUB_OUTPUT"
echo "User count implementation looks good"
fi
# Active user count in navbar is explicitly not wanted by maintainer.
# This check is permanently disabled to prevent re-filing.
echo "found=false" >> "$GITHUB_OUTPUT"
echo "Skipped — feature explicitly rejected"
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

This workflow change permanently disables the “Active user count feature” check, but the PR description/test plan are exclusively about Kube Chess and Kube Craft. Either document this CI/workflow change in the PR description (with rationale/link), or move it to a separate PR to keep scope focused and avoid surprising reviewers.

Copilot uses AI. Check for mistakes.
@clubanderson
Copy link
Copy Markdown
Collaborator Author

🔄 Auto-Applying Copilot Code Review

Copilot code review found 3 code suggestion(s) and 1 general comment(s).

@copilot Please apply all of the following code review suggestions:

  • web/src/components/cards/KubeCraft.tsx (line 288): // If the canvas has no size, we cannot reliably map mouse coordinates if (r...
  • web/src/components/cards/KubeCraft.tsx (line 289): const canvasAspect = CANVAS_WIDTH / CANVAS_HEIGHT const rectAspect = rect.wi...
  • web/src/components/cards/KubeChess.tsx (line 404): if (counter.count >= MAX_POSITIONS_EVALUATED) {

Also address these general comments:

  • .github/workflows/auto-qa.yml (line 2362): This workflow change permanently disables the “Active user count feature” check, but the PR description/test plan are ex

Push all fixes in a single commit. Run cd web && npm run build && npm run lint before committing.


Auto-generated by copilot-review-apply workflow.

clubanderson added a commit that referenced this pull request Apr 1, 2026
- KubeCraft: guard getGridCoords against zero-size bounding rect to prevent NaN
- KubeCraft: account for objectFit:contain letterboxing in expanded mode so
  clicks in padded areas are correctly rejected instead of mapping to wrong tiles
- KubeChess: fix off-by-one in minimax position limit (use >= instead of >)

Signed-off-by: Andrew Anderson <andy@clubanderson.com>
clubanderson added a commit that referenced this pull request Apr 1, 2026
- KubeCraft: guard getGridCoords against zero-size bounding rect to prevent NaN
- KubeCraft: account for objectFit:contain letterboxing in expanded mode so
  clicks in padded areas are correctly rejected instead of mapping to wrong tiles
- KubeChess: fix off-by-one in minimax position limit (use >= instead of >)

Signed-off-by: Andrew Anderson <andy@clubanderson.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dco-signoff: yes Indicates the PR's author has signed the DCO. size/L Denotes a PR that changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Card: Kube Chess (kube_chess) Card: Kube Craft (kube_craft)

3 participants