diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml
index 64e5d3c..f89d748 100644
--- a/.github/workflows/ci-cd.yml
+++ b/.github/workflows/ci-cd.yml
@@ -4,6 +4,7 @@ on:
push:
branches:
- main
+ - staging
jobs:
test:
@@ -80,7 +81,7 @@ jobs:
name: Deploy to Render
needs: [test, integration-test]
runs-on: ubuntu-latest
- if: success() && github.ref == 'refs/heads/main'
+ if: success() && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/staging')
steps:
- name: Trigger Render Deployment
run: |
diff --git a/backend/snake.db b/backend/snake.db
index 3c4e06c..7fb600a 100644
Binary files a/backend/snake.db and b/backend/snake.db differ
diff --git a/frontend/src/__tests__/fixes.test.ts b/frontend/src/__tests__/fixes.test.ts
new file mode 100644
index 0000000..52bd2e4
--- /dev/null
+++ b/frontend/src/__tests__/fixes.test.ts
@@ -0,0 +1,77 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { getScoreMultiplier, getFinalScore, createInitialState } from '@/lib/gameLogic';
+import type { GameState } from '@/types';
+
+// Mocking browser event behavior for input test logic
+describe('Bug Fix Verification', () => {
+
+ describe('Score Multiplier Logic', () => {
+ it('applies 1.5x multiplier for walls mode', () => {
+ const mode = 'walls';
+ const multiplier = getScoreMultiplier(mode);
+ expect(multiplier).toBe(1.5);
+ });
+
+ it('applies 1x multiplier for pass-through mode', () => {
+ const mode = 'pass-through';
+ const multiplier = getScoreMultiplier(mode);
+ expect(multiplier).toBe(1);
+ });
+
+ it('calculates final score correctly with multiplier', () => {
+ const state: GameState = {
+ ...createInitialState('walls'),
+ score: 100,
+ mode: 'walls'
+ };
+ const finalScore = getFinalScore(state);
+ expect(finalScore).toBe(150); // 100 * 1.5
+ });
+ });
+
+ describe('Input Event Suppression Logic', () => {
+ // Simulating the check logic used in useSnakeGame hook
+ const shouldSuppressInput = (target: any) => {
+ return target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement;
+ };
+
+ it('suppresses game controls when input is focused', () => {
+ const inputElement = document.createElement('input');
+ expect(shouldSuppressInput(inputElement)).toBe(true);
+ });
+
+ it('suppresses game controls when textarea is focused', () => {
+ const textareaElement = document.createElement('textarea');
+ expect(shouldSuppressInput(textareaElement)).toBe(true);
+ });
+
+ it('does not suppress controls when body or div is focused', () => {
+ const divElement = document.createElement('div');
+ const bodyElement = document.body;
+
+ expect(shouldSuppressInput(divElement)).toBe(false);
+ expect(shouldSuppressInput(bodyElement)).toBe(false);
+ });
+ });
+
+ describe('User Reset Logic', () => {
+ // Simulating the useEffect dependency logic
+ let gameState = createInitialState('pass-through');
+ const resetGame = () => {
+ gameState = createInitialState('pass-through');
+ };
+
+ it('resets game state when simulated user changes', () => {
+ // Simulate game progress
+ gameState.score = 50;
+ gameState.status = 'playing';
+
+ // Simulate user change trigger
+ resetGame();
+
+ expect(gameState.score).toBe(0);
+ expect(gameState.status).toBe('idle');
+ expect(gameState.snake.length).toBe(3);
+ });
+ });
+});
diff --git a/frontend/src/components/game/GameControls.tsx b/frontend/src/components/game/GameControls.tsx
index 3580f79..ae073fa 100644
--- a/frontend/src/components/game/GameControls.tsx
+++ b/frontend/src/components/game/GameControls.tsx
@@ -2,7 +2,6 @@ import React from 'react';
import { Button } from '@/components/ui/button';
import type { GameState, GameMode, Direction } from '@/types';
import { Play, Pause, RotateCcw, ArrowUp, ArrowDown, ArrowLeft, ArrowRight } from 'lucide-react';
-import { cn } from '@/lib/utils';
interface GameControlsProps {
gameState: GameState;
@@ -10,9 +9,7 @@ interface GameControlsProps {
onPause: () => void;
onResume: () => void;
onReset: () => void;
- onModeChange: (mode: GameMode) => void;
onDirectionChange: (direction: Direction) => void;
- finalScore: number;
}
export function GameControls({
@@ -21,71 +18,22 @@ export function GameControls({
onPause,
onResume,
onReset,
- onModeChange,
onDirectionChange,
- finalScore,
}: GameControlsProps) {
- const { status, mode, score } = gameState;
+ const { status } = gameState;
return (
-
- {/* Score Display */}
-
-
-
Score
-
{String(score)}
- {mode === 'walls' && (
-
1.5x multiplier active
- )}
-
-
-
- {/* Mode Selection */}
-
-
Game Mode
-
-
-
-
-
- {mode === 'pass-through'
- ? 'Snake wraps around edges'
- : 'Hit a wall = Game Over (1.5x score)'}
-
-
-
- {/* Game Controls */}
-
+
+ {/* Game Action Buttons - Compact Row */}
+
{status === 'idle' && (
)}
@@ -93,50 +41,59 @@ export function GameControls({
)}
{status === 'paused' && (
-
+ <>
+
+
+ >
)}
- {(status === 'game-over' || status === 'paused') && (
+ {status === 'game-over' && (
)}
- {/* Mobile D-Pad Controls */}
-
-
Touch Controls
-
+ {/* Mobile D-Pad Controls - Larger & Centered */}
+
+
@@ -145,41 +102,31 @@ export function GameControls({
size="icon"
onClick={() => onDirectionChange('LEFT')}
disabled={status !== 'playing'}
- className="aspect-square"
+ className="aspect-square h-16 w-16 rounded-2xl bg-background/50 border-primary/30 active:scale-95 transition-transform active:bg-primary/20"
>
-
+
-
-
-
-
-
- {/* Keyboard hints */}
-
-
Controls:
-
Arrow Keys or WASD to move
-
Space to pause • R to restart
-
);
}
+
diff --git a/frontend/src/components/game/GameOverlay.tsx b/frontend/src/components/game/GameOverlay.tsx
new file mode 100644
index 0000000..6f2198c
--- /dev/null
+++ b/frontend/src/components/game/GameOverlay.tsx
@@ -0,0 +1,48 @@
+import React from 'react';
+import { Badge } from '@/components/ui/badge';
+import { GameState, GameMode } from '@/types';
+import { cn } from '@/lib/utils';
+import { Settings2 } from 'lucide-react';
+import { Button } from '@/components/ui/button';
+
+interface GameOverlayProps {
+ score: number;
+ mode: GameMode;
+ onOpenMenu: () => void;
+}
+
+export function GameOverlay({ score, mode, onOpenMenu }: GameOverlayProps) {
+ return (
+
+
+
+ Score
+
+ {score}
+
+
+
+
+
+
+ {mode === 'pass-through' ? 'NO WALLS' : 'WALLS'}
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/components/game/MenuModal.tsx b/frontend/src/components/game/MenuModal.tsx
new file mode 100644
index 0000000..e48ca0a
--- /dev/null
+++ b/frontend/src/components/game/MenuModal.tsx
@@ -0,0 +1,90 @@
+import React from 'react';
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+ DialogDescription,
+} from "@/components/ui/dialog";
+import { Button } from '@/components/ui/button';
+import { GameMode } from '@/types';
+import { cn } from '@/lib/utils';
+import { Keyboard, MousePointer2 } from 'lucide-react';
+
+interface MenuModalProps {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ currentMode: GameMode;
+ onModeChange: (mode: GameMode) => void;
+ gameStatus: string;
+}
+
+export function MenuModal({
+ open,
+ onOpenChange,
+ currentMode,
+ onModeChange,
+ gameStatus
+}: MenuModalProps) {
+ return (
+
+ );
+}
diff --git a/frontend/src/components/layout/Header.tsx b/frontend/src/components/layout/Header.tsx
index 527e99a..c0730dc 100644
--- a/frontend/src/components/layout/Header.tsx
+++ b/frontend/src/components/layout/Header.tsx
@@ -1,4 +1,4 @@
-import React, { useState } from 'react';
+import React from 'react';
import { Button } from '@/components/ui/button';
import {
DropdownMenu,
@@ -7,102 +7,94 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
-import { AuthModal } from '@/components/auth/AuthModal';
import { useAuthContext } from '@/contexts/AuthContext';
import { User, LogOut, Trophy, Gamepad2 } from 'lucide-react';
interface HeaderProps {
activeTab: string;
onTabChange: (tab: string) => void;
+ onAuthRequest: () => void;
}
-export function Header({ activeTab, onTabChange }: HeaderProps) {
- const [authModalOpen, setAuthModalOpen] = useState(false);
+export function Header({ activeTab, onTabChange, onAuthRequest }: HeaderProps) {
const { user, isAuthenticated, logout } = useAuthContext();
return (
- <>
-
-
-
- {/* Logo */}
-
-
-
-
-
- SNAKE
-
+
-
-
- >
+
+
);
}
diff --git a/frontend/src/hooks/useSnakeGame.ts b/frontend/src/hooks/useSnakeGame.ts
index bb764ae..61ddb78 100644
--- a/frontend/src/hooks/useSnakeGame.ts
+++ b/frontend/src/hooks/useSnakeGame.ts
@@ -107,9 +107,20 @@ export function useSnakeGame(initialMode: GameMode = 'pass-through'): UseSnakeGa
};
}, [gameState.status, gameState.speed, processDirectionQueue]);
+ // Reset game when user changes (new login/logout)
+ useEffect(() => {
+ setGameState(createInitialState(initialMode));
+ directionQueueRef.current = [];
+ }, [user?.username]); // Use username as trigger
+
// Keyboard controls
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
+ // Prevent game controls when typing in inputs
+ if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
+ return;
+ }
+
if (gameState.status !== 'playing') return;
const keyToDirection: Record
= {
diff --git a/frontend/src/pages/Index.tsx b/frontend/src/pages/Index.tsx
index b2a08ef..51fcb07 100644
--- a/frontend/src/pages/Index.tsx
+++ b/frontend/src/pages/Index.tsx
@@ -5,61 +5,91 @@ import { GameBoard } from '@/components/game/GameBoard';
import { GameControls } from '@/components/game/GameControls';
import { Leaderboard } from '@/components/game/Leaderboard';
import { SpectatorView } from '@/components/game/SpectatorView';
+import { GameOverlay } from '@/components/game/GameOverlay';
+import { MenuModal } from '@/components/game/MenuModal';
+import { AuthModal } from '@/components/auth/AuthModal';
import { useSnakeGame } from '@/hooks/useSnakeGame';
function GamePage() {
const [activeTab, setActiveTab] = useState('play');
+ const [menuOpen, setMenuOpen] = useState(false);
+ const [authOpen, setAuthOpen] = useState(false);
const game = useSnakeGame('pass-through');
+ // Pause game when modals are open
+ React.useEffect(() => {
+ if (menuOpen || authOpen) {
+ game.pauseGame();
+ }
+ }, [menuOpen, authOpen]);
+
return (
-
-
+
+
setAuthOpen(true)}
+ />
-
+
{activeTab === 'play' && (
-
-
- {/* Game Board - Constrained height to prevent scrolling */}
-
+
+
+ {/* Game Controls & Info Header */}
+
+ setMenuOpen(true)}
+ />
+
- {/* Controls Sidebar */}
-
-
+ {/* Game Area Container - Dynamic Fitting */}
+
+
+ {/* Controls Area */}
+
+
+
)}
{activeTab === 'leaderboard' && (
-
+
)}
{activeTab === 'spectate' && (
-
+
)}
- {/* Footer */}
-
+
{
+ game.setMode(mode);
+ setMenuOpen(false);
+ }}
+ gameStatus={game.gameState.status}
+ />
+
+
);
}