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
61 changes: 61 additions & 0 deletions guidelines/Guidelines.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
**Add your own guidelines here**
<!--

System Guidelines

Use this file to provide the AI with rules and guidelines you want it to follow.
This template outlines a few examples of things you can add. You can add your own sections and format it to suit your needs

TIP: More context isn't always better. It can confuse the LLM. Try and add the most important rules you need

# General guidelines

Any general rules you want the AI to follow.
For example:

* Only use absolute positioning when necessary. Opt for responsive and well structured layouts that use flexbox and grid by default
* Refactor code as you go to keep code clean
* Keep file sizes small and put helper functions and components in their own files.

--------------

# Design system guidelines
Rules for how the AI should make generations look like your company's design system

Additionally, if you select a design system to use in the prompt box, you can reference
your design system's components, tokens, variables and components.
For example:

* Use a base font-size of 14px
* Date formats should always be in the format “Jun 10”
* The bottom toolbar should only ever have a maximum of 4 items
* Never use the floating action button with the bottom toolbar
* Chips should always come in sets of 3 or more
* Don't use a dropdown if there are 2 or fewer options

You can also create sub sections and add more specific details
For example:


## Button
The Button component is a fundamental interactive element in our design system, designed to trigger actions or navigate
users through the application. It provides visual feedback and clear affordances to enhance user experience.

### Usage
Buttons should be used for important actions that users need to take, such as form submissions, confirming choices,
or initiating processes. They communicate interactivity and should have clear, action-oriented labels.

### Variants
* Primary Button
* Purpose : Used for the main action in a section or page
* Visual Style : Bold, filled with the primary brand color
* Usage : One primary button per section to guide users toward the most important action
* Secondary Button
* Purpose : Used for alternative or supporting actions
* Visual Style : Outlined with the primary color, transparent background
* Usage : Can appear alongside a primary button for less important actions
* Tertiary Button
* Purpose : Used for the least important actions
* Visual Style : Text-only with no border, using primary color
* Usage : For actions that should be available but not emphasized
-->
12 changes: 2 additions & 10 deletions src/features/snake-game/SnakeGame.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import { ScoreDisplay } from './components/ScoreDisplay';
import { DecorativeBolt } from './components/DecorativeBolt';
import { GameTabs } from './components/GameTabs';
import { HighScoreBadge } from './components/HighScoreBadge';
import { ComboBadge } from './components/ComboBadge';
import { Countdown } from './components/Countdown';
import { PauseOverlay } from './components/PauseOverlay';
import { useSwipe } from './hooks/useSwipe';
Expand All @@ -29,6 +28,7 @@ export function SnakeGame({ className = '' }: SnakeGameProps) {
mode,
leaderboard,
combo,
lastEatTime,
particles,
shakeKey,
actions,
Expand Down Expand Up @@ -89,12 +89,9 @@ export function SnakeGame({ className = '' }: SnakeGameProps) {
status={status}
score={score}
mode={mode}
difficulty={difficulty}
leaderboard={leaderboard}
onStart={handleStartClick}
onSaveScore={actions.saveToLeaderboard}
onSetDifficulty={actions.setDifficulty}
onSetMode={actions.setMode}
/>

{status === 'countdown' && <Countdown onFinish={actions.startGame} />}
Expand All @@ -121,18 +118,13 @@ export function SnakeGame({ className = '' }: SnakeGameProps) {
</>
)}

{status === 'playing' && combo > 1 && (
<div className="absolute top-3 right-3 sm:top-6 sm:right-6 pointer-events-none">
<ComboBadge combo={combo} />
</div>
)}
</div>
</div>

<div className="flex flex-col items-end justify-between w-full lg:w-[180px] gap-6">
<div className="flex flex-col gap-6 w-full">
<GameControls status={status} onDirection={handleDirectionClick} />
<ScoreDisplay score={score} mode={mode} />
<ScoreDisplay score={score} mode={mode} combo={combo} lastEatTime={lastEatTime} />
</div>

<button
Expand Down
15 changes: 0 additions & 15 deletions src/features/snake-game/components/ComboBadge.tsx

This file was deleted.

56 changes: 8 additions & 48 deletions src/features/snake-game/components/GameOverlay.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,25 @@
import { memo, useState } from 'react';
import type { GameStatus, GameMode, LeaderboardEntry, Difficulty } from '../types';
import { DIFFICULTY_LABELS, MODE_LABELS } from '../constants';
import type { GameStatus, GameMode, LeaderboardEntry } from '../types';
import { DIFFICULTY_LABELS } from '../constants';

const MEDALS = ['#FFD700', '#C0C0C0', '#CD7F32'] as const;
const DIFFICULTIES: Difficulty[] = ['easy', 'normal', 'hard'];
const MODES: GameMode[] = ['casual', 'competitive'];

interface GameOverlayProps {
status: GameStatus;
score: number;
mode: GameMode;
difficulty: Difficulty;
leaderboard: LeaderboardEntry[];
onStart: () => void;
onSaveScore: (name: string) => void;
onSetDifficulty: (d: Difficulty) => void;
onSetMode: (m: GameMode) => void;
}

export const GameOverlay = memo(function GameOverlay({
status,
score,
mode,
difficulty,
leaderboard,
onStart,
onSaveScore,
onSetDifficulty,
onSetMode,
}: GameOverlayProps) {
const [playerName, setPlayerName] = useState('');
const [saved, setSaved] = useState(false);
Expand Down Expand Up @@ -144,44 +136,12 @@ export const GameOverlay = memo(function GameOverlay({
</div>
)}

<div className="pt-2 space-y-3">
<div className="flex flex-wrap items-center justify-center gap-1.5 font-['Fira_Code',sans-serif] text-[11px]">
{DIFFICULTIES.map((d) => (
<button
key={d}
onClick={() => onSetDifficulty(d)}
className={`px-2 py-1 rounded transition-colors ${
difficulty === d
? 'bg-[#43D9AD] text-[#020618]'
: 'text-[#90a1b9] hover:text-[#f8fafc] border border-[#314158]'
}`}
>
{DIFFICULTY_LABELS[d]}
</button>
))}
<span className="text-[#314158] mx-0.5">|</span>
{MODES.map((m) => (
<button
key={m}
onClick={() => onSetMode(m)}
className={`px-2 py-1 rounded transition-colors ${
mode === m
? 'bg-[#9d4edd] text-[#f8fafc]'
: 'text-[#90a1b9] hover:text-[#f8fafc] border border-[#314158]'
}`}
>
{MODE_LABELS[m]}
</button>
))}
</div>

<button
onClick={handleRestart}
className="bg-[#ffb86a] hover:bg-[#ffb86a]/90 transition-colors px-6 py-2.5 rounded-lg font-['Fira_Code',sans-serif] font-[450] text-[#020618] text-sm focus-visible:outline-2 focus-visible:outline-[#f8fafc] focus-visible:outline-offset-2"
>
{buttonLabel}
</button>
</div>
<button
onClick={handleRestart}
className="bg-[#ffb86a] hover:bg-[#ffb86a]/90 transition-colors px-6 py-2.5 rounded-lg font-['Fira_Code',sans-serif] font-[450] text-[#020618] text-sm focus-visible:outline-2 focus-visible:outline-[#f8fafc] focus-visible:outline-offset-2"
>
{buttonLabel}
</button>
</div>
</div>
);
Expand Down
25 changes: 23 additions & 2 deletions src/features/snake-game/components/ScoreDisplay.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
import { memo } from 'react';
import { FOOD_TO_WIN_CASUAL } from '../constants';
import { FOOD_TO_WIN_CASUAL, COMBO_WINDOW_MS } from '../constants';
import type { GameMode } from '../types';

interface ScoreDisplayProps {
score: number;
mode: GameMode;
combo: number;
lastEatTime: number;
}

export const ScoreDisplay = memo(function ScoreDisplay({ score, mode }: ScoreDisplayProps) {
export const ScoreDisplay = memo(function ScoreDisplay({
score,
mode,
combo,
lastEatTime,
}: ScoreDisplayProps) {
if (mode === 'competitive') {
const showCombo = combo > 1;
const multiplier = Math.min(combo, 5);
return (
<div className="w-full">
<p className="font-['Fira_Code',sans-serif] text-[#f8fafc] text-sm mb-1">
Expand All @@ -17,6 +26,18 @@ export const ScoreDisplay = memo(function ScoreDisplay({ score, mode }: ScoreDis
<p className="font-['Fira_Code',sans-serif] text-[#43D9AD] text-2xl font-bold">
{score}
</p>
<div className="h-8 mt-2">
{showCombo && (
<div
key={lastEatTime}
className="snake-combo font-['Fira_Code',sans-serif] text-[#b14eff] text-xs inline-flex items-center gap-1.5"
style={{ animationDuration: `${COMBO_WINDOW_MS}ms` }}
>
<span>combo x{multiplier}</span>
<span className="snake-combo-bar" style={{ animationDuration: `${COMBO_WINDOW_MS}ms` }} />
</div>
)}
</div>
</div>
);
}
Expand Down
2 changes: 2 additions & 0 deletions src/features/snake-game/hooks/useSnakeGame.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export function useSnakeGame() {
const mode = useGameStore((s) => s.mode);
const leaderboard = useGameStore((s) => s.leaderboard);
const combo = useGameStore((s) => s.combo);
const lastEatTime = useGameStore((s) => s.lastEatTime);
const particles = useGameStore((s) => s.particles);
const shakeKey = useGameStore((s) => s.shakeKey);
const startGame = useGameStore((s) => s.startGame);
Expand Down Expand Up @@ -90,6 +91,7 @@ export function useSnakeGame() {
mode,
leaderboard,
combo,
lastEatTime,
particles,
shakeKey,
remainingFood,
Expand Down
5 changes: 3 additions & 2 deletions src/features/snake-game/store/useGameStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -276,9 +276,10 @@ export const useGameStore = create<GameStore>((set, get) => ({
if (food && newHead.x === food.x && newHead.y === food.y) {
const { score, highScore } = get();
const foodCfg = FOOD_TYPES[food.type];
const isCompetitive = mode === 'competitive';
const withinCombo = tickTime - lastEatTime < COMBO_WINDOW_MS && lastEatTime > 0;
const newCombo = withinCombo ? combo + 1 : 1;
const comboMultiplier = Math.min(newCombo, 5);
const newCombo = isCompetitive && withinCombo ? combo + 1 : 1;
const comboMultiplier = isCompetitive ? Math.min(newCombo, 5) : 1;
const newScore = score + foodCfg.points * comboMultiplier;
const newFood = generateRandomFood(newSnake);
const newParticles = [...particles, ...spawnParticles(food.x, food.y, foodCfg.color, 10)];
Expand Down
25 changes: 25 additions & 0 deletions src/styles/theme.css
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,31 @@
animation: combo-pop 0.25s ease-out;
}

@keyframes snake-combo-fade {
0% { opacity: 1; }
60% { opacity: 1; }
100% { opacity: 0; }
}

.snake-combo {
animation: snake-combo-fade linear forwards;
}

@keyframes snake-combo-bar {
0% { transform: scaleX(1); }
100% { transform: scaleX(0); }
}

.snake-combo-bar {
display: inline-block;
width: 40px;
height: 3px;
background: #b14eff;
border-radius: 2px;
transform-origin: left center;
animation: snake-combo-bar linear forwards;
}

@keyframes countdown-pop {
0% { transform: scale(0.4); opacity: 0; }
40% { transform: scale(1.1); opacity: 1; }
Expand Down
Loading