From b70c67b5b2ad15fa25ae9c3b97f51aea1fb2d960 Mon Sep 17 00:00:00 2001
From: "gpt-engineer-app[bot]"
<159125892+gpt-engineer-app[bot]@users.noreply.github.com>
Date: Fri, 2 Jan 2026 22:01:17 +0000
Subject: [PATCH] Changes
---
src/components/MobileControlsDock.tsx | 40 ++++++++++
src/pages/Breakout.tsx | 43 ++++++-----
src/pages/Pacman.tsx | 55 ++++++++------
src/pages/Snake.tsx | 57 ++++++++------
src/pages/SnakeMultiplayer.tsx | 105 +++++++++++++++++---------
src/pages/Tetris.tsx | 53 +++++++------
6 files changed, 228 insertions(+), 125 deletions(-)
create mode 100644 src/components/MobileControlsDock.tsx
diff --git a/src/components/MobileControlsDock.tsx b/src/components/MobileControlsDock.tsx
new file mode 100644
index 0000000..3ec2eee
--- /dev/null
+++ b/src/components/MobileControlsDock.tsx
@@ -0,0 +1,40 @@
+import React from "react";
+
+type MobileControlsDockProps = {
+ children: React.ReactNode;
+ /** When false, renders nothing. */
+ visible?: boolean;
+ /** Optional override for the outer z-index. */
+ zIndexClassName?: string;
+};
+
+/**
+ * Fixed bottom dock for mobile game controls.
+ * - Always visible on screen
+ * - Respects iOS safe-area
+ * - Keeps design tokens (no raw colors)
+ */
+export function MobileControlsDock({
+ children,
+ visible = true,
+ zIndexClassName = "z-[70]",
+}: MobileControlsDockProps) {
+ if (!visible) return null;
+
+ return (
+
+ );
+}
+
+/** Spacer to prevent fixed dock from covering page content in scroll containers. */
+export function MobileControlsSpacer({ heightClassName = "h-40" }: { heightClassName?: string }) {
+ return ;
+}
diff --git a/src/pages/Breakout.tsx b/src/pages/Breakout.tsx
index 8bc3fbf..85d0580 100644
--- a/src/pages/Breakout.tsx
+++ b/src/pages/Breakout.tsx
@@ -7,6 +7,7 @@ import GlitchCrash from '@/components/GlitchCrash';
import { Maximize2, Minimize2 } from 'lucide-react';
import { useBrowserFullscreen } from '@/hooks/useGameDimensions';
import GameTouchButton from '@/components/GameTouchButton';
+import { MobileControlsDock, MobileControlsSpacer } from '@/components/MobileControlsDock';
const PADDLE_HEIGHT = 14;
const BRICK_ROWS = 6;
@@ -53,7 +54,8 @@ const Breakout = () => {
const isMobile = window.innerWidth < 768;
if (isMobile) {
const maxWidth = window.innerWidth - 32;
- const maxHeight = window.innerHeight - 280;
+ // Reserve space for header + fixed controls dock on mobile
+ const maxHeight = window.innerHeight - 380;
const aspectRatio = 480 / 580;
let width = maxWidth;
let height = width / aspectRatio;
@@ -279,24 +281,29 @@ const Breakout = () => {
)}
{isMobile && (
-
-
-
SCORE
{score.toLocaleString()}
-
HIGH
{highScore.toLocaleString()}
-
-
-
- {gameStarted && !gameOver && (
-
-
←
-
-
→
+ <>
+
+
+
+
+
SCORE
{score.toLocaleString()}
+
HIGH
{highScore.toLocaleString()}
+
+
+
+
+ {gameStarted && !gameOver ? (
+
+ ←
+
+ →
+
+ ) : (
+
+ )}
- )}
- {(!gameStarted || gameOver) && (
-
- )}
-
+
+ >
)}
diff --git a/src/pages/Pacman.tsx b/src/pages/Pacman.tsx
index be3a881..6417222 100644
--- a/src/pages/Pacman.tsx
+++ b/src/pages/Pacman.tsx
@@ -7,6 +7,7 @@ import GlitchCrash from '@/components/GlitchCrash';
import { Maximize2, Minimize2 } from 'lucide-react';
import { useBrowserFullscreen } from '@/hooks/useGameDimensions';
import GameTouchButton from '@/components/GameTouchButton';
+import { MobileControlsDock, MobileControlsSpacer } from '@/components/MobileControlsDock';
const GRID_WIDTH = 21;
const GRID_HEIGHT = 21;
@@ -76,7 +77,8 @@ const Pacman = () => {
const isMobile = window.innerWidth < 768;
if (isMobile) {
const maxWidth = window.innerWidth - 40;
- const maxHeight = window.innerHeight - 300;
+ // Reserve space for header + fixed controls dock on mobile
+ const maxHeight = window.innerHeight - 420;
return Math.min(Math.floor(maxWidth / GRID_WIDTH), Math.floor(maxHeight / GRID_HEIGHT), 16);
}
return isFullscreen ? 26 : 20;
@@ -352,29 +354,36 @@ const Pacman = () => {
)}
{isMobile && (
-
-
-
SCORE
{score.toLocaleString()}
-
HIGH
{highScore.toLocaleString()}
-
-
- {gameStarted && !gameOver && !gameComplete && (
-
-
-
setNextDirection('up')} className="p-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg">↑
-
-
setNextDirection('left')} className="p-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg">←
-
-
setNextDirection('right')} className="p-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg">→
-
-
setNextDirection('down')} className="p-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg">↓
-
+ <>
+
+
+
+
+
SCORE
{score.toLocaleString()}
+
HIGH
{highScore.toLocaleString()}
+
+
+
+ {gameStarted && !gameOver && !gameComplete ? (
+
+
+
setNextDirection('up')} className="p-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg">↑
+
+
setNextDirection('left')} className="p-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg">←
+
+
setNextDirection('right')} className="p-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg">→
+
+
setNextDirection('down')} className="p-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg">↓
+
+
+ ) : (
+
+ )}
- )}
- {(!gameStarted || gameOver || gameComplete) && (
-
- )}
-
+
+ >
)}
diff --git a/src/pages/Snake.tsx b/src/pages/Snake.tsx
index 7dd55d3..aea09f8 100644
--- a/src/pages/Snake.tsx
+++ b/src/pages/Snake.tsx
@@ -7,6 +7,7 @@ import GlitchCrash from '@/components/GlitchCrash';
import { Maximize2, Minimize2 } from 'lucide-react';
import { useBrowserFullscreen } from '@/hooks/useGameDimensions';
import GameTouchButton from '@/components/GameTouchButton';
+import { MobileControlsDock, MobileControlsSpacer } from '@/components/MobileControlsDock';
const GRID_SIZE = 20;
const TICK_SPEED = 120;
@@ -42,7 +43,8 @@ const Snake = () => {
const isMobile = window.innerWidth < 768;
if (isMobile) {
const maxWidth = window.innerWidth - 40;
- const maxHeight = window.innerHeight - 300;
+ // Reserve space for header + fixed controls dock on mobile
+ const maxHeight = window.innerHeight - 420;
return Math.min(Math.floor(maxWidth / GRID_SIZE), Math.floor(maxHeight / GRID_SIZE), 18);
}
return isFullscreen ? 28 : 24;
@@ -313,31 +315,36 @@ const Snake = () => {
{/* Mobile Controls */}
{isMobile && (
-
-
-
SCORE
{score.toLocaleString()}
-
HIGH
{highScore.toLocaleString()}
-
-
- {gameStarted && !gameOver && !gameComplete && (
-
-
-
directionQueueRef.current.push('up')} className="p-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg">↑
-
-
directionQueueRef.current.push('left')} className="p-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg">←
-
-
directionQueueRef.current.push('right')} className="p-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg">→
-
-
directionQueueRef.current.push('down')} className="p-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg">↓
-
+ <>
+
+
+
+
+
SCORE
{score.toLocaleString()}
+
HIGH
{highScore.toLocaleString()}
+
+
+
+ {gameStarted && !gameOver && !gameComplete ? (
+
+
+
directionQueueRef.current.push('up')} className="p-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg">↑
+
+
directionQueueRef.current.push('left')} className="p-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg">←
+
+
directionQueueRef.current.push('right')} className="p-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg">→
+
+
directionQueueRef.current.push('down')} className="p-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg">↓
+
+
+ ) : (
+
+ )}
- )}
- {(!gameStarted || gameOver || gameComplete) && (
-
- )}
-
+
+ >
)}
diff --git a/src/pages/SnakeMultiplayer.tsx b/src/pages/SnakeMultiplayer.tsx
index 86c3b97..cb4d68f 100644
--- a/src/pages/SnakeMultiplayer.tsx
+++ b/src/pages/SnakeMultiplayer.tsx
@@ -4,6 +4,7 @@ import { useSettings } from '@/contexts/SettingsContext';
import { Link } from 'react-router-dom';
import { Maximize2, Minimize2 } from 'lucide-react';
import { useBrowserFullscreen } from '@/hooks/useGameDimensions';
+import { MobileControlsDock, MobileControlsSpacer } from '@/components/MobileControlsDock';
const GRID_SIZE = 20;
const TICK_SPEED = 120;
@@ -44,7 +45,8 @@ const SnakeMultiplayer = () => {
const isMobile = window.innerWidth < 768;
if (isMobile) {
const maxWidth = window.innerWidth - 40;
- const maxHeight = window.innerHeight - 300;
+ // Reserve space for header + fixed controls dock on mobile
+ const maxHeight = window.innerHeight - 420;
return Math.min(Math.floor(maxWidth / GRID_SIZE), Math.floor(maxHeight / GRID_SIZE), 16);
}
return isFullscreen ? 24 : 20;
@@ -292,44 +294,75 @@ const SnakeMultiplayer = () => {
{renderGrid()}
-
- {/* Player 1 Stats */}
-
-
-
-
PLAYER 1 (WASD)
+ {!isMobile && (
+
+ {/* Player 1 Stats */}
+
+
+
{players[0].score}
+ {!players[0].alive &&
DEAD
}
-
{players[0].score}
- {!players[0].alive &&
DEAD
}
-
-
- {/* Player 2 Stats */}
-
-
-
-
PLAYER 2 (↑↓←→)
+
+ {/* Player 2 Stats */}
+
+
+
{players[1].score}
+ {!players[1].alive &&
DEAD
}
-
{players[1].score}
- {!players[1].alive &&
DEAD
}
+
+
+
CONTROLS
+
P1: W A S D
+
P2: ↑ ↓ ← →
+
P: Pause
+
+
+ {!gameStarted || gameOver ? (
+
+ ) : (
+
+ )}
-
-
-
CONTROLS
-
P1: W A S D
-
P2: ↑ ↓ ← →
-
P: Pause
-
-
- {!gameStarted || gameOver ? (
-
- ) : (
-
- )}
-
+ )}
+
+ {isMobile && (
+ <>
+
+
+
+
+
+ {!gameStarted || gameOver ? (
+
+ ) : (
+
+ )}
+
+
+ P1: WASD
+ P2: ↑↓←→
+
+
+
+ >
+ )}
{/* Game Over / Pause Overlay */}
diff --git a/src/pages/Tetris.tsx b/src/pages/Tetris.tsx
index 7c8c0ba..2e8de09 100644
--- a/src/pages/Tetris.tsx
+++ b/src/pages/Tetris.tsx
@@ -7,6 +7,7 @@ import GlitchCrash from '@/components/GlitchCrash';
import { Maximize2, Minimize2 } from 'lucide-react';
import { useBrowserFullscreen } from '@/hooks/useGameDimensions';
import GameTouchButton from '@/components/GameTouchButton';
+import { MobileControlsDock, MobileControlsSpacer } from '@/components/MobileControlsDock';
const BOARD_WIDTH = 10;
const BOARD_HEIGHT = 20;
@@ -79,7 +80,8 @@ const Tetris = () => {
const isMobile = window.innerWidth < 768;
if (isMobile) {
const maxWidth = window.innerWidth - 40;
- const maxHeight = window.innerHeight - 320;
+ // Reserve space for header + fixed controls dock on mobile
+ const maxHeight = window.innerHeight - 440;
return Math.min(Math.floor(maxWidth / BOARD_WIDTH), Math.floor(maxHeight / BOARD_HEIGHT), 22);
}
return isFullscreen ? 30 : 24;
@@ -326,29 +328,34 @@ const Tetris = () => {
)}
{isMobile && (
-
-
-
SCORE
{score.toLocaleString()}
-
HIGH
{highScore.toLocaleString()}
-
-
- {gameStarted && !gameOver && !gameComplete && (
-
-
-
↑
-
-
←
-
DROP
-
→
-
-
↓
-
+ <>
+
+
+
+
+
SCORE
{score.toLocaleString()}
+
HIGH
{highScore.toLocaleString()}
+
+
+
+ {gameStarted && !gameOver && !gameComplete ? (
+
+
+
↑
+
+
←
+
DROP
+
→
+
+
↓
+
+
+ ) : (
+
+ )}
- )}
- {(!gameStarted || gameOver || gameComplete) && (
-
- )}
-
+
+ >
)}
{!isMobile && (gameOver || isPaused || gameComplete) && gameStarted && (