Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/ci-cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ on:
push:
branches:
- main
- staging

jobs:
test:
Expand Down Expand Up @@ -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: |
Expand Down
Binary file modified backend/snake.db
Binary file not shown.
77 changes: 77 additions & 0 deletions frontend/src/__tests__/fixes.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
});
147 changes: 47 additions & 100 deletions frontend/src/components/game/GameControls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,14 @@ 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;
onStart: () => void;
onPause: () => void;
onResume: () => void;
onReset: () => void;
onModeChange: (mode: GameMode) => void;
onDirectionChange: (direction: Direction) => void;
finalScore: number;
}

export function GameControls({
Expand All @@ -21,122 +18,82 @@ export function GameControls({
onPause,
onResume,
onReset,
onModeChange,
onDirectionChange,
finalScore,
}: GameControlsProps) {
const { status, mode, score } = gameState;
const { status } = gameState;

return (
<div className="space-y-4">
{/* Score Display */}
<div className="bg-card rounded-lg p-4 neon-box-secondary">
<div className="text-center">
<p className="text-muted-foreground text-sm uppercase tracking-wider mb-1">Score</p>
<p className="font-display text-4xl text-secondary text-glow-secondary tabular-nums">{String(score)}</p>
{mode === 'walls' && (
<p className="text-xs text-muted-foreground mt-1">1.5x multiplier active</p>
)}
</div>
</div>

{/* Mode Selection */}
<div className="bg-card rounded-lg p-4 border border-border">
<p className="text-sm text-muted-foreground mb-3 font-medium">Game Mode</p>
<div className="grid grid-cols-2 gap-2">
<Button
variant={mode === 'pass-through' ? 'default' : 'outline'}
size="sm"
onClick={() => onModeChange('pass-through')}
disabled={status === 'playing'}
className={cn(
"font-display text-xs",
mode === 'pass-through' && "neon-box"
)}
>
Pass Through
</Button>
<Button
variant={mode === 'walls' ? 'default' : 'outline'}
size="sm"
onClick={() => onModeChange('walls')}
disabled={status === 'playing'}
className={cn(
"font-display text-xs",
mode === 'walls' && "neon-box"
)}
>
Walls
</Button>
</div>
<p className="text-xs text-muted-foreground mt-2">
{mode === 'pass-through'
? 'Snake wraps around edges'
: 'Hit a wall = Game Over (1.5x score)'}
</p>
</div>

{/* Game Controls */}
<div className="space-y-2">
<div className="space-y-2">
{/* Game Action Buttons - Compact Row */}
<div className="flex gap-2 justify-center">
{status === 'idle' && (
<Button
onClick={onStart}
className="w-full font-display arcade-button neon-box"
size="lg"
className="flex-1 font-display arcade-button neon-box h-10 text-base"
size="sm"
>
<Play className="w-5 h-5 mr-2" />
Start Game
<Play className="w-4 h-4 mr-2" />
START
</Button>
)}

{status === 'playing' && (
<Button
onClick={onPause}
variant="secondary"
className="w-full font-display arcade-button neon-box-secondary"
size="lg"
className="flex-1 font-display arcade-button neon-box-secondary h-10 text-base"
size="sm"
>
<Pause className="w-5 h-5 mr-2" />
Pause
<Pause className="w-4 h-4 mr-2" />
PAUSE
</Button>
)}

{status === 'paused' && (
<Button
onClick={onResume}
className="w-full font-display arcade-button neon-box"
size="lg"
>
<Play className="w-5 h-5 mr-2" />
Resume
</Button>
<>
<Button
onClick={onResume}
className="flex-1 font-display arcade-button neon-box h-10"
size="sm"
>
<Play className="w-4 h-4 mr-2" />
RESUME
</Button>
<Button
onClick={onReset}
variant="outline"
className="px-4 border-destructive text-destructive hover:bg-destructive/10 h-10"
>
<RotateCcw className="w-4 h-4" />
</Button>
</>
)}

{(status === 'game-over' || status === 'paused') && (
{status === 'game-over' && (
<Button
onClick={onReset}
variant="outline"
className="w-full font-display"
className="flex-1 font-display h-10 text-base neon-box-primary bg-primary/20 hover:bg-primary/30 text-primary border-primary"
size="sm"
>
<RotateCcw className="w-4 h-4 mr-2" />
New Game
PLAY AGAIN
</Button>
)}
</div>

{/* Mobile D-Pad Controls */}
<div className="bg-card rounded-lg p-4 border border-border md:hidden">
<p className="text-sm text-muted-foreground mb-3 text-center">Touch Controls</p>
<div className="grid grid-cols-3 gap-2 max-w-[180px] mx-auto">
{/* Mobile D-Pad Controls - Larger & Centered */}
<div className="bg-card/50 backdrop-blur-sm rounded-xl p-2 border border-border/50 shadow-xl md:hidden">
<div className="grid grid-cols-3 gap-3 max-w-[240px] mx-auto">
<div />
<Button
variant="outline"
size="icon"
onClick={() => onDirectionChange('UP')}
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"
>
<ArrowUp className="w-5 h-5" />
<ArrowUp className="w-8 h-8" />
</Button>
<div />

Expand All @@ -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"
>
<ArrowLeft className="w-5 h-5" />
<ArrowLeft className="w-8 h-8" />
</Button>
<div />
<Button
variant="outline"
size="icon"
onClick={() => onDirectionChange('RIGHT')}
onClick={() => onDirectionChange('DOWN')}
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"
>
<ArrowRight className="w-5 h-5" />
<ArrowDown className="w-8 h-8" />
</Button>

<div />
<Button
variant="outline"
size="icon"
onClick={() => onDirectionChange('DOWN')}
onClick={() => onDirectionChange('RIGHT')}
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"
>
<ArrowDown className="w-5 h-5" />
<ArrowRight className="w-8 h-8" />
</Button>
<div />
</div>
</div>

{/* Keyboard hints */}
<div className="hidden md:block text-center text-xs text-muted-foreground space-y-1">
<p className="font-medium">Controls:</p>
<p>Arrow Keys or WASD to move</p>
<p>Space to pause • R to restart</p>
</div>
</div>
);
}

48 changes: 48 additions & 0 deletions frontend/src/components/game/GameOverlay.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="absolute top-2 left-0 w-full px-4 z-10 flex justify-between items-start pointer-events-none">
<div className="flex flex-col gap-2 pointer-events-auto">
<div className="bg-background/80 backdrop-blur-md rounded-lg px-3 py-1.5 border border-primary/20 shadow-[0_0_15px_rgba(255,255,255,0.05)] neon-box-secondary">
<span className="text-[10px] text-muted-foreground uppercase tracking-widest block font-display">Score</span>
<span className="text-2xl font-bold font-display text-secondary text-glow-secondary leading-none">
{score}
</span>
</div>
</div>

<div className="flex gap-2 pointer-events-auto">
<Badge
variant="outline"
className={cn(
"bg-background/80 backdrop-blur-md border-primary/30 font-display text-xs h-9",
mode === 'walls' ? "text-destructive border-destructive/50" : "text-primary"
)}
>
{mode === 'pass-through' ? 'NO WALLS' : 'WALLS'}
</Badge>

<Button
variant="outline"
size="icon"
onClick={onOpenMenu}
className="h-9 w-9 bg-background/80 backdrop-blur-md border-primary/30 hover:bg-primary/20"
>
<Settings2 className="h-4 w-4" />
</Button>
</div>
</div>
);
}
Loading